diff --git a/.gitignore b/.gitignore index 08040ba..4f002b6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ celerybeat-schedule .coverage *,cover coverage.xml +.noseids # Gettext object file: *.mo diff --git a/circle/acl/models.py b/circle/acl/models.py index bffc57c..2bce06d 100644 --- a/circle/acl/models.py +++ b/circle/acl/models.py @@ -71,6 +71,17 @@ class AclBase(Model): """Define permission levels for Users/Groups per object.""" object_level_set = GenericRelation(ObjectLevel) + def clone_acl(self, other): + """Clone full ACL from other object.""" + assert self.id != other.id or type(self) != type(other) + self.object_level_set.clear() + for i in other.object_level_set.all(): + ol = self.object_level_set.create(level=i.level) + for j in i.users.all(): + ol.users.add(j) + for j in i.groups.all(): + ol.groups.add(j) + @classmethod def get_level_object(cls, level): @@ -229,7 +240,7 @@ class AclBase(Model): levelfilter, content_type=ct, level__weight__gte=level.weight).distinct() clsfilter = Q(object_level_set__in=ols.all()) - return cls.objects.filter(clsfilter) + return cls.objects.filter(clsfilter).distinct() def save(self, *args, **kwargs): super(AclBase, self).save(*args, **kwargs) diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index cf91088..531dfe1 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -514,9 +514,18 @@ LOGIN_REDIRECT_URL = "/" AGENT_DIR = get_env_variable( 'DJANGO_AGENT_DIR', join(unicode(expanduser("~")), 'agent')) + # AGENT_DIR is the root directory for the agent. + # The directory structure SHOULD be: + # /home/username/agent + # |-- agent-linux + # | |-- .git + # | +-- ... + # |-- agent-win + # | +-- agent-win-%(version).exe + # try: - git_env = {'GIT_DIR': join(AGENT_DIR, '.git')} + git_env = {'GIT_DIR': join(join(AGENT_DIR, "agent-linux"), '.git')} AGENT_VERSION = check_output( ('git', 'log', '-1', r'--pretty=format:%h', 'HEAD'), env=git_env) except: diff --git a/circle/common/models.py b/circle/common/models.py index 3b80758..c5c4a7b 100644 --- a/circle/common/models.py +++ b/circle/common/models.py @@ -32,6 +32,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db.models import ( CharField, DateTimeField, ForeignKey, NullBooleanField ) +from django.template import defaultfilters from django.utils import timezone from django.utils.encoding import force_text from django.utils.functional import Promise @@ -48,7 +49,15 @@ class WorkerNotFound(Exception): pass +def get_error_msg(exception): + try: + return unicode(exception) + except UnicodeDecodeError: + return unicode(str(exception), encoding='utf-8', errors='replace') + + def activitycontextimpl(act, on_abort=None, on_commit=None): + result = None try: try: yield act @@ -61,7 +70,7 @@ def activitycontextimpl(act, on_abort=None, on_commit=None): result = create_readable( ugettext_noop("Failure."), ugettext_noop("Unhandled exception: %(error)s"), - error=unicode(e)) + error=get_error_msg(e)) raise except: logger.exception("Failed activity %s" % unicode(act)) @@ -428,6 +437,14 @@ class HumanReadableObject(object): admin_text_template = admin_text_template._proxy____args[0] self.user_text_template = user_text_template self.admin_text_template = admin_text_template + for k, v in params.iteritems(): + try: + v = timezone.datetime.strptime( + v, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.UTC()) + except (ValueError, TypeError): # Mock raises TypeError + pass + if isinstance(v, timezone.datetime): + params[k] = defaultfilters.date(v, "DATETIME_FORMAT") self.params = params @classmethod @@ -444,24 +461,27 @@ class HumanReadableObject(object): def from_dict(cls, d): return None if d is None else cls(**d) - def get_admin_text(self): - if self.admin_text_template == "": + def _get_parsed_text(self, key): + value = getattr(self, key) + if value == "": return "" try: - return _(self.admin_text_template) % self.params + return _(value) % self.params + except KeyError: + logger.exception("Can't render %s '%s' %% %s", + key, value, unicode(self.params)) + raise + + def get_admin_text(self): + try: + return self._get_parsed_text("admin_text_template") except KeyError: - logger.exception("Can't render admin_text_template '%s' %% %s", - self.admin_text_template, unicode(self.params)) return self.get_user_text() def get_user_text(self): - if self.user_text_template == "": - return "" try: - return _(self.user_text_template) % self.params + return self._get_parsed_text("user_text_template") except KeyError: - logger.exception("Can't render user_text_template '%s' %% %s", - self.user_text_template, unicode(self.params)) return self.user_text_template def get_text(self, user): diff --git a/circle/common/operations.py b/circle/common/operations.py index aeb6f3b..87c22ab 100644 --- a/circle/common/operations.py +++ b/circle/common/operations.py @@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception logger = getLogger(__name__) +class SubOperationMixin(object): + required_perms = () + + def create_activity(self, parent, user, kwargs): + if not parent: + raise TypeError("SubOperation can only be called with " + "parent_activity specified.") + return super(SubOperationMixin, self).create_activity( + parent, user, kwargs) + + class Operation(object): """Base class for VM operations. """ @@ -36,6 +47,10 @@ class Operation(object): abortable = False has_percentage = False + @classmethod + def get_activity_code_suffix(cls): + return cls.id + def __call__(self, **kwargs): return self.call(**kwargs) @@ -62,6 +77,8 @@ class Operation(object): parent_activity = auxargs.pop('parent_activity') if parent_activity and user is None and not skip_auth_check: user = parent_activity.user + if user is None: # parent was a system call + skip_auth_check = True # check for unexpected keyword arguments argspec = getargspec(self._operation) @@ -232,7 +249,7 @@ class OperatedMixin(object): operation could be found. """ for op in getattr(self, operation_registry_name, {}).itervalues(): - if has_suffix(activity_code, op.activity_code_suffix): + if has_suffix(activity_code, op.get_activity_code_suffix()): return op(self) else: return None diff --git a/circle/common/tests/test_models.py b/circle/common/tests/test_models.py index 0e71aa0..8bdba74 100644 --- a/circle/common/tests/test_models.py +++ b/circle/common/tests/test_models.py @@ -22,6 +22,7 @@ from mock import MagicMock from .models import TestClass from ..models import HumanSortField +from ..models import activitycontextimpl class MethodCacheTestCase(TestCase): @@ -80,3 +81,22 @@ class TestHumanSortField(TestCase): test_result = HumanSortField.get_normalized_value(obj, val) self.assertEquals(test_result, result) + + +class ActivityContextTestCase(TestCase): + class MyException(Exception): + pass + + def test_unicode(self): + act = MagicMock() + gen = activitycontextimpl(act) + gen.next() + with self.assertRaises(self.MyException): + gen.throw(self.MyException(u'test\xe1')) + + def test_str(self): + act = MagicMock() + gen = activitycontextimpl(act) + gen.next() + with self.assertRaises(self.MyException): + gen.throw(self.MyException('test\xbe')) diff --git a/circle/common/tests/test_operations.py b/circle/common/tests/test_operations.py index e75e09e..20edd6c 100644 --- a/circle/common/tests/test_operations.py +++ b/circle/common/tests/test_operations.py @@ -27,9 +27,7 @@ class OperationTestCase(TestCase): class AbortEx(Exception): pass - op = Operation(MagicMock()) - op.activity_code_suffix = 'test' - op.id = 'test' + op = TestOp(MagicMock()) op.async_operation = MagicMock( apply_async=MagicMock(side_effect=AbortEx)) @@ -44,9 +42,7 @@ class OperationTestCase(TestCase): class AbortEx(Exception): pass - op = Operation(MagicMock()) - op.activity_code_suffix = 'test' - op.id = 'test' + op = TestOp(MagicMock()) with patch.object(Operation, 'create_activity', side_effect=AbortEx): with patch.object(Operation, 'check_precond') as chk_pre: try: @@ -55,9 +51,7 @@ class OperationTestCase(TestCase): self.assertTrue(chk_pre.called) def test_auth_check_on_non_system_call(self): - op = Operation(MagicMock()) - op.activity_code_suffix = 'test' - op.id = 'test' + op = TestOp(MagicMock()) user = MagicMock() with patch.object(Operation, 'check_auth') as check_auth: with patch.object(Operation, 'check_precond'), \ @@ -67,9 +61,7 @@ class OperationTestCase(TestCase): check_auth.assert_called_with(user) def test_no_auth_check_on_system_call(self): - op = Operation(MagicMock()) - op.activity_code_suffix = 'test' - op.id = 'test' + op = TestOp(MagicMock()) with patch.object(Operation, 'check_auth', side_effect=AssertionError): with patch.object(Operation, 'check_precond'), \ patch.object(Operation, 'create_activity'), \ @@ -77,39 +69,25 @@ class OperationTestCase(TestCase): op.call(system=True) def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self): - class KwargOp(Operation): - activity_code_suffix = 'test' - id = 'test' - - def _operation(self, **kwargs): - pass - - op = KwargOp(MagicMock()) - with patch.object(KwargOp, 'create_activity'), \ - patch.object(KwargOp, '_exec_op'): + op = TestOp(MagicMock()) + with patch.object(TestOp, 'create_activity'), \ + patch.object(TestOp, '_exec_op'): op.call(system=True, foo=42) def test_exception_for_unexpected_arguments(self): - class TestOp(Operation): - activity_code_suffix = 'test' - id = 'test' - - def _operation(self): - pass - op = TestOp(MagicMock()) with patch.object(TestOp, 'create_activity'), \ patch.object(TestOp, '_exec_op'): - self.assertRaises(TypeError, op.call, system=True, foo=42) + self.assertRaises(TypeError, op.call, system=True, bar=42) def test_exception_for_missing_arguments(self): - class TestOp(Operation): - activity_code_suffix = 'test' - id = 'test' - - def _operation(self, foo): - pass - op = TestOp(MagicMock()) with patch.object(TestOp, 'create_activity'): self.assertRaises(TypeError, op.call, system=True) + + +class TestOp(Operation): + id = 'test' + + def _operation(self, foo): + pass diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index 2e4ca73..4000354 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -18,6 +18,7 @@ from __future__ import absolute_import from datetime import timedelta +from urlparse import urlparse from django.contrib.auth.forms import ( AuthenticationForm, PasswordResetForm, SetPasswordForm, @@ -39,6 +40,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.html import escape from django.utils.translation import ugettext_lazy as _ from sizefield.widgets import FileSizeWidget from django.core.urlresolvers import reverse_lazy @@ -69,9 +71,7 @@ priority_choices = ( ) -class VmSaveForm(forms.Form): - name = forms.CharField(max_length=100, label=_('Name'), - help_text=_('Human readable name of template.')) +class NoFormTagMixin(object): @property def helper(self): @@ -80,6 +80,27 @@ class VmSaveForm(forms.Form): return helper +class OperationForm(NoFormTagMixin, forms.Form): + pass + + +class VmSaveForm(OperationForm): + name = forms.CharField(max_length=100, label=_('Name'), + help_text=_('Human readable name of template.')) + + def __init__(self, *args, **kwargs): + default = kwargs.pop('default', None) + clone = kwargs.pop('clone', False) + super(VmSaveForm, self).__init__(*args, **kwargs) + if default: + self.fields['name'].initial = default + if clone: + self.fields.insert(2, "clone", forms.BooleanField( + required=False, label=_("Clone template permissions"), + help_text=_("Clone the access list of parent template. Useful " + "for updating a template."))) + + class VmCustomizeForm(forms.Form): name = forms.CharField(widget=forms.TextInput(attrs={ 'class': "form-control", @@ -185,7 +206,7 @@ class VmCustomizeForm(forms.Form): del self.cleaned_data[name] -class GroupCreateForm(forms.ModelForm): +class GroupCreateForm(NoFormTagMixin, forms.ModelForm): description = forms.CharField(label=_("Description"), required=False, widget=forms.Textarea(attrs={'rows': 3})) @@ -224,9 +245,8 @@ class GroupCreateForm(forms.ModelForm): @property def helper(self): - helper = FormHelper(self) + helper = super(GroupCreateForm, self).helper helper.add_input(Submit("submit", _("Create"))) - helper.form_tag = False return helper class Meta: @@ -234,7 +254,7 @@ class GroupCreateForm(forms.ModelForm): fields = ('name', ) -class GroupProfileUpdateForm(forms.ModelForm): +class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm): def __init__(self, *args, **kwargs): new_groups = kwargs.pop('new_groups', None) @@ -253,9 +273,8 @@ class GroupProfileUpdateForm(forms.ModelForm): @property def helper(self): - helper = FormHelper(self) + helper = super(GroupProfileUpdateForm, self).helper helper.add_input(Submit("submit", _("Save"))) - helper.form_tag = False return helper def save(self, commit=True): @@ -270,17 +289,16 @@ class GroupProfileUpdateForm(forms.ModelForm): fields = ('description', 'org_id') -class HostForm(forms.ModelForm): +class HostForm(NoFormTagMixin, forms.ModelForm): def setowner(self, user): self.instance.owner = user - def __init__(self, *args, **kwargs): - super(HostForm, self).__init__(*args, **kwargs) - self.helper = FormHelper(self) - self.helper.form_show_labels = False - self.helper.form_tag = False - self.helper.layout = Layout( + @property + def helper(self): + helper = super(HostForm, self).helper + helper.form_show_labels = False + helper.layout = Layout( Div( Div( # host Div( @@ -337,6 +355,7 @@ class HostForm(forms.ModelForm): ), ), ) + return helper class Meta: model = Host @@ -717,7 +736,7 @@ class LeaseForm(forms.ModelForm): model = Lease -class VmRenewForm(forms.Form): +class VmRenewForm(OperationForm): force = forms.BooleanField(required=False, label=_( "Set expiration times even if they are shorter than " @@ -737,14 +756,27 @@ class VmRenewForm(forms.Form): self.fields['lease'].widget = HiddenInput() self.fields['save'].widget = HiddenInput() - @property - def helper(self): - helper = FormHelper(self) - helper.form_tag = False - return helper + +class VmMigrateForm(forms.Form): + live_migration = forms.BooleanField( + required=False, initial=True, label=_("Live migration"), + help_text=_( + "Live migration is a way of moving virtual machines between " + "hosts with a service interruption of at most some seconds. " + "Please note that it can take very long and cause " + "much network traffic in case of busy machines.")) + + def __init__(self, *args, **kwargs): + choices = kwargs.pop('choices') + default = kwargs.pop('default') + super(VmMigrateForm, self).__init__(*args, **kwargs) + + self.fields.insert(0, 'to_node', forms.ModelChoiceField( + queryset=choices, initial=default, required=False, + widget=forms.RadioSelect(), label=_("Node"))) -class VmStateChangeForm(forms.Form): +class VmStateChangeForm(OperationForm): interrupt = forms.BooleanField(required=False, label=_( "Forcibly interrupt all running activities."), @@ -763,31 +795,25 @@ class VmStateChangeForm(forms.Form): self.fields['interrupt'].widget = HiddenInput() self.fields['new_state'].initial = status - @property - def helper(self): - helper = FormHelper(self) - helper.form_tag = False - return helper - -class RedeployForm(forms.Form): +class RedeployForm(OperationForm): with_emergency_change_state = forms.BooleanField( required=False, initial=True, label=_("use emergency state change")) - @property - def helper(self): - helper = FormHelper(self) - helper.form_tag = False - return helper - -class VmCreateDiskForm(forms.Form): +class VmCreateDiskForm(OperationForm): name = forms.CharField(max_length=100, label=_("Name")) size = forms.CharField( widget=FileSizeWidget, initial=(10 << 30), label=_('Size'), help_text=_('Size of disk to create in bytes or with units ' 'like MB or GB.')) + def __init__(self, *args, **kwargs): + default = kwargs.pop('default', None) + super(VmCreateDiskForm, self).__init__(*args, **kwargs) + if default: + self.fields['name'].initial = default + def clean_size(self): size_in_bytes = self.cleaned_data.get("size") if not size_in_bytes.isdigit() and len(size_in_bytes) > 0: @@ -795,14 +821,8 @@ class VmCreateDiskForm(forms.Form): " GB or MB!")) return size_in_bytes - @property - def helper(self): - helper = FormHelper(self) - helper.form_tag = False - return helper - -class VmDiskResizeForm(forms.Form): +class VmDiskResizeForm(OperationForm): size = forms.CharField( widget=FileSizeWidget, initial=(10 << 30), label=_('Size'), help_text=_('Size to resize the disk in bytes or with units ' @@ -835,27 +855,60 @@ class VmDiskResizeForm(forms.Form): @property def helper(self): - helper = FormHelper(self) - helper.form_tag = False + helper = super(VmDiskResizeForm, self).helper if self.disk: helper.layout = Layout( - HTML(_(" %s") % self.disk), + HTML(_(" %s") % escape(self.disk)), Field('disk'), Field('size')) return helper -class VmDownloadDiskForm(forms.Form): - name = forms.CharField(max_length=100, label=_("Name")) - url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) +class VmDiskRemoveForm(OperationForm): + def __init__(self, *args, **kwargs): + choices = kwargs.pop('choices') + self.disk = kwargs.pop('default') + + super(VmDiskRemoveForm, self).__init__(*args, **kwargs) + + self.fields.insert(0, 'disk', forms.ModelChoiceField( + queryset=choices, initial=self.disk, required=True, + empty_label=None, label=_('Disk'))) + if self.disk: + self.fields['disk'].widget = HiddenInput() @property def helper(self): - helper = FormHelper(self) - helper.form_tag = False + helper = super(VmDiskRemoveForm, self).helper + if self.disk: + helper.layout = Layout( + AnyTag( + "div", + HTML(_(" %s") % escape(self.disk)), + css_class="form-group", + ), + Field("disk"), + ) return helper -class VmAddInterfaceForm(forms.Form): +class VmDownloadDiskForm(OperationForm): + name = forms.CharField(max_length=100, label=_("Name"), required=False) + url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) + + def clean(self): + cleaned_data = super(VmDownloadDiskForm, self).clean() + if not cleaned_data['name']: + if cleaned_data['url']: + cleaned_data['name'] = urlparse( + cleaned_data['url']).path.split('/')[-1] + if not cleaned_data['name']: + raise forms.ValidationError( + _("Could not find filename in URL, " + "please specify a name explicitly.")) + return cleaned_data + + +class VmAddInterfaceForm(OperationForm): def __init__(self, *args, **kwargs): choices = kwargs.pop('choices') super(VmAddInterfaceForm, self).__init__(*args, **kwargs) @@ -867,11 +920,19 @@ class VmAddInterfaceForm(forms.Form): field.empty_label = _('No more networks.') self.fields['vlan'] = field - @property - def helper(self): - helper = FormHelper(self) - helper.form_tag = False - return helper + +class VmDeployForm(OperationForm): + + def __init__(self, *args, **kwargs): + choices = kwargs.pop('choices', None) + + super(VmDeployForm, self).__init__(*args, **kwargs) + + if choices is not None: + self.fields.insert(0, 'node', forms.ModelChoiceField( + queryset=choices, required=False, label=_('Node'), help_text=_( + "Deploy virtual machine to this node " + "(blank allows scheduling automatically)."))) class CircleAuthenticationForm(AuthenticationForm): diff --git a/circle/dashboard/models.py b/circle/dashboard/models.py index ff05e65..fd5fdef 100644 --- a/circle/dashboard/models.py +++ b/circle/dashboard/models.py @@ -236,6 +236,9 @@ class GroupProfile(AclBase): help_text=_('Unique identifier of the group at the organization.')) description = TextField(blank=True) + def __unicode__(self): + return self.group.name + def save(self, *args, **kwargs): if not self.org_id: self.org_id = None diff --git a/circle/dashboard/static/dashboard/dashboard.js b/circle/dashboard/static/dashboard/dashboard.js index 87184d3..4677a02 100644 --- a/circle/dashboard/static/dashboard/dashboard.js +++ b/circle/dashboard/static/dashboard/dashboard.js @@ -411,14 +411,25 @@ $(function () { $(this).removeClass("btn-default").addClass("btn-primary"); return false; }); + + // vm migrate select for node + $(document).on("click", "#vm-migrate-node-list li", function(e) { + var li = $(this).closest('li'); + if (li.find('input').attr('disabled')) + return true; + $('#vm-migrate-node-list li').removeClass('panel-primary'); + li.addClass('panel-primary').find('input').prop("checked", true); + return true; + }); + }); function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { return '' + - '' + - ' ' + name + - '' + + (is_last ? ' list-group-item-last' : '') + '">' + + '' + + ' ' + safe_tags_replace(name) + + '' + ' ' + host + '' + '
' + (fav ? '' : @@ -430,14 +441,14 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateGroupHTML(url, name, is_last) { return ''+ - ' '+ name + + ' '+ safe_tags_replace(name) + ''; } function generateNodeHTML(name, icon, _status, url, is_last) { return '' + '' + - ' ' + name + + ' ' + safe_tags_replace(name) + '' + '
' + '
'; @@ -445,7 +456,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) { function generateNodeTagHTML(name, icon, _status, label , url) { return '' + - ' ' + name + + ' ' + safe_tags_replace(name) + ' '; } @@ -619,7 +630,7 @@ function addModalConfirmation(func, data) { } function clientInstalledAction(location) { - setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/"); + setCookie('downloaded_client', true, 365 * 24 * 60 * 60 * 1000, "/"); window.location.href = location; $('#confirmation-modal').modal("hide"); } @@ -687,3 +698,17 @@ $(function() { return choice.children().html(); } }); + +var tagsToReplace = { + '&': '&', + '<': '<', + '>': '>' +}; + +function replaceTag(tag) { + return tagsToReplace[tag] || tag; +} + +function safe_tags_replace(str) { + return str.replace(/[&<>]/g, replaceTag); +} diff --git a/circle/dashboard/static/dashboard/dashboard.less b/circle/dashboard/static/dashboard/dashboard.less index 3f8f088..f04139a 100644 --- a/circle/dashboard/static/dashboard/dashboard.less +++ b/circle/dashboard/static/dashboard/dashboard.less @@ -216,7 +216,7 @@ html { } #vm-list-rename-name, #node-list-rename-name, #group-list-rename-name { - max-width: 100px; + max-width: 150px; } .label-tag { @@ -539,7 +539,7 @@ footer a, footer a:hover, footer a:visited { } #dashboard-template-list a small { - max-width: 50%; + max-width: 45%; float: left; padding-top: 2px; text-overflow: ellipsis; @@ -699,7 +699,7 @@ textarea[name="new_members"] { .dashboard-vm-details-connect-command { /* for mobile view */ - margin-bottom: 20px; + padding-bottom: 20px; } #store-list-list { @@ -1014,6 +1014,14 @@ textarea[name="new_members"] { cursor: pointer } +#vm-details-resources-disk { + padding: 2px 5px 10px 5px; +} + +#vm-details-start-template-tour { + margin-right: 5px; +} + #vm-activity-state { margin-bottom: 15px; } @@ -1027,6 +1035,24 @@ textarea[name="new_members"] { color: orange; } +.introjs-skipbutton { + color: #333; +} + +.introjs-button:focus { + text-decoration: none; + color: #333; + outline: none; +} + +.introjs-button:hover:not(.introjs-disabled) { + color: #428bca; +} + +.introjs-tooltip { + min-width: 250px; +} + #vm-info-pane { margin-bottom: 20px; } @@ -1053,7 +1079,8 @@ textarea[name="new_members"] { max-width: 100%; } -#vm-list-table tbody td:nth-child(3) { +#vm-list-table td.state, +#vm-list-table td.memory { white-space: nowrap; } @@ -1064,3 +1091,16 @@ textarea[name="new_members"] { .disk-resize-btn { margin-right: 5px; } + +#vm-migrate-node-list li { + cursor: pointer; +} + +.group-list-table .actions, +.group-list-table .admin, +.group-list-table .number_of_users, +.group-list-table .pk { + width: 1px; + white-space: nowrap; + text-align: center; +} diff --git a/circle/dashboard/static/dashboard/disk-list.js b/circle/dashboard/static/dashboard/disk-list.js deleted file mode 100644 index 3971ce8..0000000 --- a/circle/dashboard/static/dashboard/disk-list.js +++ /dev/null @@ -1,23 +0,0 @@ -$(function() { - $(".disk-list-disk-percentage").each(function() { - var disk = $(this).data("disk-pk"); - var element = $(this); - refreshDisk(disk, element); - }); -}); - -function refreshDisk(disk, element) { - $.get("/dashboard/disk/" + disk + "/status/", function(result) { - if(result.percentage === null || result.failed == "True") { - location.reload(); - } else { - var diff = result.percentage - parseInt(element.html()); - var refresh = 5 - diff; - refresh = refresh < 1 ? 1 : (result.percentage === 0 ? 1 : refresh); - if(isNaN(refresh)) refresh = 2; // this should not happen - - element.html(result.percentage); - setTimeout(function() {refreshDisk(disk, element);}, refresh * 1000); - } - }); -} diff --git a/circle/dashboard/static/dashboard/group-details.js b/circle/dashboard/static/dashboard/group-details.js index 7e788fc..ac1578d 100644 --- a/circle/dashboard/static/dashboard/group-details.js +++ b/circle/dashboard/static/dashboard/group-details.js @@ -14,7 +14,7 @@ data: {'new_name': name}, headers: {"X-CSRFToken": getCookie('csrftoken')}, success: function(data, textStatus, xhr) { - $("#group-details-h1-name").html(data.new_name).show(); + $("#group-details-h1-name").text(data['new_name']).show(); $('#group-details-rename').hide(); // addMessage(data['message'], "success"); }, diff --git a/circle/dashboard/static/dashboard/group-list.js b/circle/dashboard/static/dashboard/group-list.js index 462a0ee..4fec8cf 100644 --- a/circle/dashboard/static/dashboard/group-list.js +++ b/circle/dashboard/static/dashboard/group-list.js @@ -3,6 +3,7 @@ $(function() { $("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-column-name", $(this).closest("tr")).hide(); $("#group-list-rename", $(this).closest("tr")).css('display', 'inline'); + $("#group-list-rename").find("input").select(); }); /* rename ajax */ diff --git a/circle/dashboard/static/dashboard/introjs/intro.min.js b/circle/dashboard/static/dashboard/introjs/intro.min.js new file mode 100644 index 0000000..e152d52 --- /dev/null +++ b/circle/dashboard/static/dashboard/introjs/intro.min.js @@ -0,0 +1,27 @@ +(function(p,f){"object"===typeof exports?f(exports):"function"===typeof define&&define.amd?define(["exports"],f):f(p)})(this,function(p){function f(a){this._targetElement=a;this._options={nextLabel:"Next →",prevLabel:"← Back",skipLabel:"Skip",doneLabel:"Done",tooltipPosition:"bottom",tooltipClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,scrollToElement:!0,overlayOpacity:0.8}}function r(a){if(null==a||"object"!=typeof a|| +"undefined"!=typeof a.nodeType)return a;var b={},c;for(c in a)b[c]=r(a[c]);return b}function s(){this._direction="forward";"undefined"===typeof this._currentStep?this._currentStep=0:++this._currentStep;if(this._introItems.length<=this._currentStep)"function"===typeof this._introCompleteCallback&&this._introCompleteCallback.call(this),t.call(this,this._targetElement);else{var a=this._introItems[this._currentStep];"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this, +a.element);A.call(this,a)}}function x(){this._direction="backward";if(0===this._currentStep)return!1;var a=this._introItems[--this._currentStep];"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element);A.call(this,a)}function t(a){var b=a.querySelector(".introjs-overlay");if(null!=b){b.style.opacity=0;setTimeout(function(){b.parentNode&&b.parentNode.removeChild(b)},500);(a=a.querySelector(".introjs-helperLayer"))&&a.parentNode.removeChild(a);(a=document.querySelector(".introjsFloatingElement"))&& +a.parentNode.removeChild(a);if(a=document.querySelector(".introjs-showElement"))a.className=a.className.replace(/introjs-[a-zA-Z]+/g,"").replace(/^\s+|\s+$/g,"");if((a=document.querySelectorAll(".introjs-fixParent"))&&0 a.active").className="";d.querySelector('.introjs-bullets li > a[data-stepnumber="'+a.step+'"]').className="active";y.style.opacity=1;e&&(e.style.opacity=1)},350)}else{var k=document.createElement("div"),m=document.createElement("div"),j=document.createElement("div"),n=document.createElement("div"),l=document.createElement("div"),f=document.createElement("div");k.className="introjs-helperLayer";v.call(c,k);this._targetElement.appendChild(k);m.className= +"introjs-arrow";n.className="introjs-tooltiptext";n.innerHTML=a.intro;l.className="introjs-bullets";!1===this._options.showBullets&&(l.style.display="none");var p=document.createElement("ul");b=0;for(var u=this._introItems.length;bj)b.className+=" introjs-fixParent";b=b.parentNode}b=a.element.getBoundingClientRect();!(0<=b.top&&0<=b.left&&b.bottom+80<=window.innerHeight&&b.right<=window.innerWidth)&&!0===this._options.scrollToElement&&(j=a.element.getBoundingClientRect(),b=void 0!=window.innerWidth?window.innerHeight:document.documentElement.clientHeight,m=j.bottom-(j.bottom-j.top),j=j.bottom-b,0>m||a.element.clientHeight>b?window.scrollBy(0,m-30):window.scrollBy(0, +j+100));"undefined"!==typeof this._introAfterChangeCallback&&this._introAfterChangeCallback.call(this,a.element)}function z(a,b){var c="";a.currentStyle?c=a.currentStyle[b]:document.defaultView&&document.defaultView.getComputedStyle&&(c=document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return c&&c.toLowerCase?c.toLowerCase():c}function D(a){var b=document.createElement("div"),c="",d=this;b.className="introjs-overlay";if("body"===a.tagName.toLowerCase())c+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;", +b.setAttribute("style",c);else{var e=h(a);e&&(c+="width: "+e.width+"px; height:"+e.height+"px; top:"+e.top+"px;left: "+e.left+"px;",b.setAttribute("style",c))}a.appendChild(b);b.onclick=function(){!0==d._options.exitOnOverlayClick&&(t.call(d,a),void 0!=d._introExitCallback&&d._introExitCallback.call(d))};setTimeout(function(){c+="opacity: "+d._options.overlayOpacity.toString()+";";b.setAttribute("style",c)},10);return!0}function h(a){var b={};b.width=a.offsetWidth;b.height=a.offsetHeight;for(var c= +0,d=0;a&&!isNaN(a.offsetLeft)&&!isNaN(a.offsetTop);)c+=a.offsetLeft,d+=a.offsetTop,a=a.offsetParent;b.top=d;b.left=c;return b}var u=function(a){if("object"===typeof a)return new f(a);if("string"===typeof a){if(a=document.querySelector(a))return new f(a);throw Error("There is no element with given selector.");}return new f(document.body)};u.version="0.9.0";u.fn=f.prototype={clone:function(){return new f(this)},setOption:function(a,b){this._options[a]=b;return this},setOptions:function(a){var b=this._options, +c={},d;for(d in b)c[d]=b[d];for(d in a)c[d]=a[d];this._options=c;return this},start:function(){a:{var a=this._targetElement,b=[],c=this;if(this._options.steps)for(var d=[],e=0,d=this._options.steps.length;ed.length)break a;e=0;for(f=d.length;etd,tr.introjs-showElement>th{z-index:9999999 !important}.introjs-relativePosition,tr.introjs-showElement>td,tr.introjs-showElement>th{position:relative}.introjs-helperLayer{position:absolute;z-index:9999998;background-color:#FFF;background-color:rgba(255,255,255,.9);border:1px solid #777;border:1px solid rgba(0,0,0,.5);border-radius:4px;box-shadow:0 2px 15px rgba(0,0,0,.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperNumberLayer{position:absolute;top:-16px;left:-16px;z-index:9999999999 !important;padding:2px;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:bold;color:white;text-align:center;text-shadow:1px 1px 1px rgba(0,0,0,.3);background:#ff3019;background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff3019),color-stop(100%,#cf0404));background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);width:20px;height:20px;line-height:20px;border:3px solid white;border-radius:50%;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019',endColorstr='#cf0404',GradientType=0);filter:progid:DXImageTransform.Microsoft.Shadow(direction=135,strength=2,color=ff0000);box-shadow:0 2px 5px rgba(0,0,0,.4)}.introjs-arrow{border:5px solid white;content:'';position:absolute}.introjs-arrow.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-right{top:-10px;right:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-middle{top:-10px;left:50%;margin-left:-5px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.bottom{bottom:-10px;border-top-color:white;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-tooltip{position:absolute;padding:10px;background-color:white;min-width:200px;max-width:300px;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,.4);-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.introjs-tooltipbuttons{text-align:right}.introjs-button{position:relative;overflow:visible;display:inline-block;padding:.3em .8em;border:1px solid #d4d4d4;margin:0;text-decoration:none;text-shadow:1px 1px 0 #fff;font:11px/normal sans-serif;color:#333;white-space:nowrap;cursor:pointer;outline:0;background-color:#ececec;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f4f4f4),to(#ececec));background-image:-moz-linear-gradient(#f4f4f4,#ececec);background-image:-o-linear-gradient(#f4f4f4,#ececec);background-image:linear-gradient(#f4f4f4,#ececec);-webkit-background-clip:padding;-moz-background-clip:padding;-o-background-clip:padding-box;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;zoom:1;*display:inline;margin-top:10px}.introjs-button:hover{border-color:#bcbcbc;text-decoration:none;box-shadow:0 1px 1px #e3e3e3}.introjs-button:focus,.introjs-button:active{background-image:-webkit-gradient(linear,0 0,0 100%,from(#ececec),to(#f4f4f4));background-image:-moz-linear-gradient(#ececec,#f4f4f4);background-image:-o-linear-gradient(#ececec,#f4f4f4);background-image:linear-gradient(#ececec,#f4f4f4)}.introjs-button::-moz-focus-inner{padding:0;border:0}.introjs-skipbutton{margin-right:5px;color:#7a7a7a}.introjs-prevbutton{-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em;border-right:0}.introjs-nextbutton{-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-disabled,.introjs-disabled:hover,.introjs-disabled:focus{color:#9a9a9a;border-color:#d4d4d4;box-shadow:none;cursor:default;background-color:#f4f4f4;background-image:none;text-decoration:none}.introjs-bullets{text-align:center}.introjs-bullets ul{clear:both;margin:15px auto 0;padding:0;display:inline-block}.introjs-bullets ul li{list-style:none;float:left;margin:0 2px}.introjs-bullets ul li a{display:block;width:6px;height:6px;background:#ccc;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;text-decoration:none}.introjs-bullets ul li a:hover{background:#999}.introjs-bullets ul li a.active{background:#999}.introjsFloatingElement{position:absolute;height:0;width:0;left:50%;top:50%} + + +.introjs-helperLayer *, +.introjs-helperLayer *:before, +.introjs-helperLayer *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + -ms-box-sizing: content-box; + -o-box-sizing: content-box; + box-sizing: content-box; +} diff --git a/circle/dashboard/static/dashboard/node-details.js b/circle/dashboard/static/dashboard/node-details.js index d1e3cd8..9725ce6 100644 --- a/circle/dashboard/static/dashboard/node-details.js +++ b/circle/dashboard/static/dashboard/node-details.js @@ -15,7 +15,7 @@ $(function() { data: {'new_name': name}, headers: {"X-CSRFToken": getCookie('csrftoken')}, success: function(data, textStatus, xhr) { - $("#node-details-h1-name").html(data.new_name).show(); + $("#node-details-h1-name").text(data['new_name']).show(); $('#node-details-rename').hide(); // addMessage(data.message, "success"); }, diff --git a/circle/dashboard/static/dashboard/node-list.js b/circle/dashboard/static/dashboard/node-list.js index 4faf9a3..fcc3283 100644 --- a/circle/dashboard/static/dashboard/node-list.js +++ b/circle/dashboard/static/dashboard/node-list.js @@ -10,41 +10,6 @@ $(function() { $('.true').closest("tr").removeClass('danger'); } - /* rename */ - $("#node-list-rename-button, .node-details-rename-button").click(function() { - $("#node-list-column-name", $(this).closest("tr")).hide(); - $("#node-list-rename", $(this).closest("tr")).css('display', 'inline'); - }); - - /* rename ajax */ - $('.node-list-rename-submit').click(function() { - var row = $(this).closest("tr"); - var name = $('#node-list-rename-name', row).val(); - var url = '/dashboard/node/' + 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) { - - $("#node-list-column-name", row).html( - $("", { - 'class': "real-link", - href: "/dashboard/node/" + data.node_pk + "/", - text: data.new_name - }) - ).show(); - $('#node-list-rename', row).hide(); - // addMessage(data['message'], "success"); - }, - error: function(xhr, textStatus, error) { - addMessage("Error during renaming!", "danger"); - } - }); - return false; - }); - function statuschangeSuccess(tr){ var tspan=tr.children('.enabled').children(); var buttons=tr.children('.actions').children('.btn-group').children('.dropdown-menu').children('li').children('.node-enable'); diff --git a/circle/dashboard/static/dashboard/template-list.js b/circle/dashboard/static/dashboard/template-list.js index c6cd19b..1c6ac21 100644 --- a/circle/dashboard/static/dashboard/template-list.js +++ b/circle/dashboard/static/dashboard/template-list.js @@ -2,7 +2,7 @@ $(function() { /* for template removes buttons */ $('.template-delete').click(function() { var template_pk = $(this).data('template-pk'); - addModalConfirmation(deleteTemplate, + addModalConfirmationOrDisplayMessage(deleteTemplate, { 'url': '/dashboard/template/delete/' + template_pk + '/', 'data': [], 'template_pk': template_pk, @@ -13,7 +13,7 @@ $(function() { /* for lease removes buttons */ $('.lease-delete').click(function() { var lease_pk = $(this).data('lease-pk'); - addModalConfirmation(deleteLease, + addModalConfirmationOrDisplayMessage(deleteLease, { 'url': '/dashboard/lease/delete/' + lease_pk + '/', 'data': [], 'lease_pk': lease_pk, @@ -81,3 +81,29 @@ function deleteLease(data) { } }); } + +function addModalConfirmationOrDisplayMessage(func, data) { + $.ajax({ + type: 'GET', + url: data['url'], + data: jQuery.param(data['data']), + success: function(result) { + $('body').append(result); + $('#confirmation-modal').modal('show'); + $('#confirmation-modal').on('hidden.bs.modal', function() { + $('#confirmation-modal').remove(); + }); + $('#confirmation-modal-button').click(function() { + func(data); + $('#confirmation-modal').modal('hide'); + }); + }, + error: function(xhr, textStatus, error) { + if(xhr.status === 403) { + addMessage(gettext("Only the owners can delete the selected object."), "warning"); + } else { + addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger') + } + } + }); +} diff --git a/circle/dashboard/static/dashboard/vm-common.js b/circle/dashboard/static/dashboard/vm-common.js index 6c61112..cc749f1 100644 --- a/circle/dashboard/static/dashboard/vm-common.js +++ b/circle/dashboard/static/dashboard/vm-common.js @@ -16,19 +16,10 @@ $(function() { $('#confirmation-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').remove(); }); - - $('#vm-migrate-node-list li').click(function(e) { - var li = $(this).closest('li'); - if (li.find('input').attr('disabled')) - return true; - $('#vm-migrate-node-list li').removeClass('panel-primary'); - li.addClass('panel-primary').find('input').attr('checked', true); - return false; - }); $('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary'); } }); - return false; + e.preventDefault(); }); /* if the operation fails show the modal again */ @@ -51,7 +42,8 @@ $(function() { if(data.success) { $('a[href="#activity"]').trigger("click"); if(data.with_reload) { - location.reload(); + // when the activity check stops the page will reload + reload_vm_detail = true; } /* if there are messages display them */ diff --git a/circle/dashboard/static/dashboard/vm-details.js b/circle/dashboard/static/dashboard/vm-details.js index d485ca8..d0422e7 100644 --- a/circle/dashboard/static/dashboard/vm-details.js +++ b/circle/dashboard/static/dashboard/vm-details.js @@ -1,7 +1,8 @@ var show_all = false; var in_progress = false; var activity_hash = 5; -var Websock_native; +var Websock_native; // not sure +var reload_vm_detail = false; $(function() { /* do we need to check for new activities */ @@ -28,7 +29,7 @@ $(function() { }); /* save resources */ - $('#vm-details-resources-save').click(function() { + $('#vm-details-resources-save').click(function(e) { var error = false; $(".cpu-count-input, .ram-input").each(function() { if(!$(this)[0].checkValidity()) { @@ -61,7 +62,7 @@ $(function() { } } }); - return false; + e.preventDefault(); }); /* remove tag */ @@ -205,11 +206,11 @@ $(function() { }); /* rename in home tab */ - $(".vm-details-home-edit-name-click").click(function() { + $(".vm-details-home-edit-name-click").click(function(e) { $(".vm-details-home-edit-name-click").hide(); $("#vm-details-home-rename").show(); $("input", $("#vm-details-home-rename")).select(); - return false; + e.preventDefault(); }); /* rename ajax */ @@ -236,15 +237,15 @@ $(function() { }); /* update description click */ - $(".vm-details-home-edit-description-click").click(function() { + $(".vm-details-home-edit-description-click").click(function(e) { $(".vm-details-home-edit-description-click").hide(); $("#vm-details-home-description").show(); var ta = $("#vm-details-home-description textarea"); var tmp = ta.val(); ta.val(""); ta.focus(); - ta.val(tmp); - return false; + ta.val(tmp) + e.preventDefault(); }); /* description update ajax */ @@ -316,6 +317,24 @@ $(function() { if(Boolean($(this).data("disabled"))) return false; }); + $("#dashboard-tutorial-toggle").click(function() { + var box = $("#alert-new-template"); + var list = box.find("ol") + list.stop().slideToggle(function() { + var url = box.find("form").prop("action"); + var hidden = list.css("display") === "none"; + box.find("button i").prop("class", "fa fa-caret-" + (hidden ? "down" : "up")); + $.ajax({ + type: 'POST', + url: url, + data: {'hidden': hidden}, + headers: {"X-CSRFToken": getCookie('csrftoken')}, + success: function(re, textStatus, xhr) {} + }); + }); + return false; + }); + }); @@ -344,9 +363,6 @@ function decideActivityRefresh() { /* if something is still spinning */ if($('.timeline .activity i').hasClass('fa-spin')) check = true; - /* if there is only one activity */ - if($('#activity-timeline div[class="activity"]').length < 2) - check = true; return check; } @@ -377,8 +393,9 @@ function checkNewActivity(runs) { } else { icon.prop("class", "fa " + data.icon); } - $("#vm-details-state span").html(data.human_readable_status.toUpperCase()); - if(data.status == "RUNNING") { + $("#vm-details-state").data("status", data['status']); + $("#vm-details-state span").html(data['human_readable_status'].toUpperCase()); + if(data['status'] == "RUNNING") { if(data['connect_uri']) { $("#dashboard-vm-details-connect-button").removeClass('disabled'); } @@ -405,6 +422,7 @@ function checkNewActivity(runs) { ); } else { in_progress = false; + if(reload_vm_detail) location.reload(); } $('a[href="#activity"] i').removeClass('fa-spin'); }, diff --git a/circle/dashboard/static/dashboard/vm-tour.js b/circle/dashboard/static/dashboard/vm-tour.js index c1600f4..1c1065d 100644 --- a/circle/dashboard/static/dashboard/vm-tour.js +++ b/circle/dashboard/static/dashboard/vm-tour.js @@ -1,142 +1,129 @@ +var intro; $(function() { - $(".vm-details-start-template-tour").click(function() { - ttour = createTemplateTour(); - ttour.init(); - ttour.start(); + $("#vm-details-start-template-tour").click(function() { + intro = introJs(); + intro.setOptions({ + 'nextLabel': gettext("Next") + ' ', + 'prevLabel': ' ' + gettext("Previous"), + 'skipLabel': ' ' + gettext("End tour"), + 'doneLabel': gettext("Done"), + }); + intro.setOptions({ + 'steps': get_steps(), + }); + intro.onbeforechange(function(target) { + /* if the tab menu item is highlighted */ + if($(target).data("toggle") == "pill") { + $(target).trigger("click"); + } + + /* if anything in a tab is highlighted change to that tab */ + var tab = $(target).closest('.tab-pane:not([id^="ipv"])'); + var id = tab.prop("id"); + if(id) { + id = id.substring(1, id.length); + $('a[href="#' + id + '"]').trigger("click"); + } + }); + intro.start(); + + return false; }); -}); -function createTemplateTour() { - var ttour = new Tour({ - storage: false, - name: "template", - template: "
" + - "
" + - "

" + - "
" + - "
" + - "
" + - " " + - " ' + - " " + - "
" + - "' + - "
" + - "
", + $(document).on('click', '#vm-details-resources-save, .vm-details-home-edit-name-click, .vm-details-home-edit-description-click, a.operation', function() { + if(intro) + intro.exit(); }); +}); - ttour.addStep({ - element: "#vm-details-template-tour-button", - title: gettext("Template Tutorial Tour"), - content: "

" + gettext("Welcome to the template tutorial. In this quick tour, we gonna show you how to do the steps described above.") + "

" + - "

" + gettext('For the next tour step press the "Next" button or the right arrow (or "Back" button/left arrow for the previous step).') + "

" + - "

" + gettext("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!") + "

", - placement: "bottom", - backdrop: true, - }); - ttour.addStep({ - backdrop: true, - element: 'a[href="#home"]', - title: gettext("Home tab"), - content: gettext("In this tab you can tag your virtual machine and modify the name and description."), - placement: 'top', - onShow: function() { - $('a[href="#home"]').trigger("click"); +function get_steps() { + // if an activity is running the #ops will be refreshed + // and the intro will break + deploy_selector = "#ops"; + save_as_selector = "#ops"; + if(!$('.timeline .activity i').hasClass('fa-spin')) { + vm_status = $("#vm-details-state").data("status"); + if(vm_status === "PENDING") + deploy_selector += ' a[class*="operation-deploy"]'; + if(vm_status === "RUNNING" || vm_status === "STOPPED") + save_as_selector += ' a[class*="operation-save_as_template"]'; + } + + steps = [ + { + element: document.querySelector("#vm-details-start-template-tour"), + intro: "

" + gettext("Welcome to the template tutorial. In this quick tour, we are going to show you how to do the steps described above.") + "

" + + "

" + gettext('For the next tour step press the "Next" button or the right arrow (or "Back" button/left arrow for the previous step).') + "

" }, - }); - - ttour.addStep({ - element: 'a[href="#resources"]', - title: gettext("Resources tab"), - backdrop: true, - placement: 'top', - content: gettext("On the resources tab you can edit the CPU/RAM options and add/remove disks!"), - onShow: function() { - $('a[href="#resources"]').trigger("click"); + { + element: document.querySelector('a[href="#home"]'), + intro: gettext("In this tab you can extend the expiration date of your virtual machine, add tags and modify the name and description."), }, - }); - - ttour.addStep({ - element: '#vm-details-resources-form', - placement: 'top', - backdrop: true, - title: gettext("Resources"), - content: '

' + gettext("CPU priority") + ": " + gettext("higher is better") + "

" + - '

' + gettext("CPU count") + ": " + gettext("number of CPU cores.") + "

" + - '

' + gettext("RAM amount") + ": " + gettext("amount of RAM.") + "

", - onShow: function() { - $('a[href="#resources"]').trigger("click"); + { + element: document.querySelector('#home_name_and_description'), + intro: gettext("Please add a meaningful description to the virtual machine. Changing the name is also recommended, however you can choose a new name when saving the template."), }, - }); - - ttour.addStep({ - element: '#vm-details-resources-disk', - backdrop: true, - placement: 'top', - title: gettext("Disks"), - content: gettext("You can add empty disks, download new ones and remove existing ones here."), - onShow: function() { - $('a[href="#resources"]').trigger("click"); + { + element: document.querySelector('#home_expiration_and_lease'), + intro: gettext("You can change the lease to extend the expiration date. This will be the lease of the new template."), }, - }); - - ttour.addStep({ - element: 'a[href="#network"]', - backdrop: true, - placement: 'top', - title: gettext("Network tab"), - content: gettext('You can add new network interfaces or remove existing ones here.'), - onShow: function() { - $('a[href="#network"]').trigger("click"); + { + element: document.querySelector('a[href="#resources"]'), + intro: gettext("On the resources tab you can edit the CPU/RAM options and add/remove disks if you have required permissions."), + } + ]; + + if($("#vm-details-resources-save").length) { + steps.push( + { + element: document.querySelector('#vm-details-resources-form'), + intro: '

' + gettext("CPU priority") + ": " + + gettext("higher is better") + "

" + + "

" + gettext("CPU count") + ": " + + gettext("number of CPU cores.") + "

" + + "

" + gettext("RAM amount") + ": " + + gettext("amount of RAM.") + "

", + position: "top", + } + ); + } + + if($(".operation-create_disk").length || $(".operation-download_disk").length) { + steps.push( + { + element: document.querySelector('#vm-details-resources-disk'), + intro: gettext("You can add empty disks, download new ones and remove existing ones here."), + position: "top", + } + ); + } + + steps.push( + { + element: document.querySelector('a[href="#network"]'), + intro: gettext('You can add new network interfaces or remove existing ones here.'), }, - }); - - - ttour.addStep({ - element: "#ops", - title: ' ' + gettext("Deploy"), - placement: "left", - backdrop: true, - content: gettext("Deploy the virtual machine."), - }); - - ttour.addStep({ - element: "#vm-info-pane", - title: gettext("Connect"), - placement: "top", - backdrop: true, - content: gettext("Use the connection string or connect with your choice of client!"), - - }); - - ttour.addStep({ - element: "#vm-info-pane", - placement: "top", - title: gettext("Customize the virtual machine"), - content: gettext("After you have connected to the virtual machine do your modifications then log off."), - }); - - ttour.addStep({ - element: "#ops", - title: ' ' + gettext("Save as"), - placement: "left", - backdrop: true, - content: gettext('Press the "Save as template" button and wait until the activity finishes.'), - }); - - - ttour.addStep({ - element: ".alert-new-template", - title: gettext("Finish"), - backdrop: true, - placement: "bottom", - content: gettext("This is the last message, if something is not clear you can do the the tour again!"), - }); - - return ttour; + { + element: document.querySelector(deploy_selector), + intro: gettext("Deploy the virtual machine."), + }, + { + element: document.querySelector("#vm-info-pane"), + intro: gettext("Use the CIRCLE client or the connection string to connect to the virtual machine."), + }, + { + element: document.querySelector("#vm-info-pane"), + intro: gettext("After you have connected to the virtual machine do your modifications then log off."), + }, + { + element: document.querySelector(save_as_selector), + intro: gettext('Press the "Save as template" button and wait until the activity finishes.'), + }, + { + element: document.querySelector(".alert-new-template"), + intro: gettext("This is the last message, if something is not clear you can do the the tour again."), + } + ); + return steps; } diff --git a/circle/dashboard/static/local-logo.png b/circle/dashboard/static/local-logo.png new file mode 100644 index 0000000..b3c1c1f Binary files /dev/null and b/circle/dashboard/static/local-logo.png differ diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py index 27dc8e1..063ada3 100644 --- a/circle/dashboard/tables.py +++ b/circle/dashboard/tables.py @@ -88,18 +88,21 @@ class GroupListTable(Table): number_of_users = TemplateColumn( orderable=False, + verbose_name=_("Number of users"), template_name='dashboard/group-list/column-users.html', attrs={'th': {'class': 'group-list-table-admin'}}, ) admin = TemplateColumn( orderable=False, + verbose_name=_("Admin"), template_name='dashboard/group-list/column-admin.html', attrs={'th': {'class': 'group-list-table-admin'}}, ) actions = TemplateColumn( orderable=False, + verbose_name=_("Actions"), attrs={'th': {'class': 'group-list-table-thin'}}, template_code=('{% include "dashboard/group-list/column-' 'actions.html" with btn_size="btn-xs" %}'), diff --git a/circle/dashboard/templates/branding.html b/circle/dashboard/templates/branding.html new file mode 100644 index 0000000..500b248 --- /dev/null +++ b/circle/dashboard/templates/branding.html @@ -0,0 +1,2 @@ + + diff --git a/circle/dashboard/templates/dashboard/_disk-list-element.html b/circle/dashboard/templates/dashboard/_disk-list-element.html index 70f35af..3cbc2e1 100644 --- a/circle/dashboard/templates/dashboard/_disk-list-element.html +++ b/circle/dashboard/templates/dashboard/_disk-list-element.html @@ -1,28 +1,29 @@ {% load i18n %} {% load sizefieldtags %} - -{{ d.name }} (#{{ d.id }}) - -{% if not d.is_downloading %} - {% if not d.failed %} - {% if d.size %}{{ d.size|filesize }}{% endif %} - {% else %} -
{% trans "failed" %}
- {% endif %} -{% else %}{{ d.get_download_percentage }}%{% endif %} -{% if is_owner != False %} -
- {% if long_remove %} {% trans "Remove" %}{% endif %} - - {% if op.resize_disk %} - - - {% trans "Resize" %} - - - {% endif %} + +{{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }} + +{% if op.remove_disk %} + + + {% trans "Remove" %} + + +{% endif %} +{% if op.resize_disk %} + + + {% trans "Resize" %} + + {% endif %}
+ +{% if request.user.is_superuser %} + {% trans "File name" %}: {{ d.filename }} +{% endif %} diff --git a/circle/dashboard/templates/dashboard/_vm-mass-migrate.html b/circle/dashboard/templates/dashboard/_vm-mass-migrate.html index 6038785..bb68c06 100644 --- a/circle/dashboard/templates/dashboard/_vm-mass-migrate.html +++ b/circle/dashboard/templates/dashboard/_vm-mass-migrate.html @@ -1,6 +1,7 @@ {% extends "dashboard/mass-operate.html" %} {% load i18n %} {% load sizefieldtags %} +{% load crispy_forms_tags %} {% block formfields %} @@ -11,20 +12,20 @@ - + {% trans "This option will reschedule each virtual machine to the optimal node." %}
- {% for n in nodes %} + {% for n in form.fields.to_node.queryset.all %}
  • - + {% trans "CPU load" %}: {{ n.cpu_usage }} {% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}
    @@ -32,5 +33,6 @@
  • {% endfor %} + {{ form.live_migration|as_crispy_field }}
    {% endblock %} diff --git a/circle/dashboard/templates/dashboard/_vm-migrate.html b/circle/dashboard/templates/dashboard/_vm-migrate.html index c56515f..c8024c4 100644 --- a/circle/dashboard/templates/dashboard/_vm-migrate.html +++ b/circle/dashboard/templates/dashboard/_vm-migrate.html @@ -1,6 +1,7 @@ {% extends "dashboard/operate.html" %} {% load i18n %} {% load sizefieldtags %} +{% load crispy_forms_tags %} {% block question %}

    @@ -13,24 +14,27 @@ Choose a compute node to migrate {{obj}} to. {% block formfields %}

    + {{ form.live_migration|as_crispy_field }} {% endblock %} diff --git a/circle/dashboard/templates/dashboard/base.html b/circle/dashboard/templates/dashboard/base.html index 42e7f4d..d76aad2 100644 --- a/circle/dashboard/templates/dashboard/base.html +++ b/circle/dashboard/templates/dashboard/base.html @@ -5,10 +5,16 @@ {% block title-site %}Dashboard | CIRCLE{% endblock %} +{% block extra_link %} + + + +{% endblock %} + {% block navbar-brand %} - + {% include "branding.html" %} {% endblock %} diff --git a/circle/dashboard/templates/dashboard/confirm/ajax-node-status.html b/circle/dashboard/templates/dashboard/confirm/ajax-node-status.html index 8b96540..74b2035 100644 --- a/circle/dashboard/templates/dashboard/confirm/ajax-node-status.html +++ b/circle/dashboard/templates/dashboard/confirm/ajax-node-status.html @@ -15,11 +15,11 @@
    {% csrf_token %} - +
    -
    +
    diff --git a/circle/dashboard/templates/dashboard/confirm/ajax-remove.html b/circle/dashboard/templates/dashboard/confirm/ajax-remove.html index 60a6400..32d4402 100644 --- a/circle/dashboard/templates/dashboard/confirm/ajax-remove.html +++ b/circle/dashboard/templates/dashboard/confirm/ajax-remove.html @@ -7,7 +7,7 @@ {{ text }} {% else %} {%blocktrans with object=object%} - Are you sure you want to remove {{ member }} from {{ object }}? + Are you sure you want to remove {{ member }} from {{ object }}? {%endblocktrans%} {% endif %}
    diff --git a/circle/dashboard/templates/dashboard/confirm/node-flush.html b/circle/dashboard/templates/dashboard/confirm/node-flush.html index 0ed51a1..a26e325 100644 --- a/circle/dashboard/templates/dashboard/confirm/node-flush.html +++ b/circle/dashboard/templates/dashboard/confirm/node-flush.html @@ -23,9 +23,9 @@
    {% csrf_token %} - {% trans "Back" %} - - + {% trans "Back" %} + +
    diff --git a/circle/dashboard/templates/dashboard/confirm/node-status.html b/circle/dashboard/templates/dashboard/confirm/node-status.html index 3b6b56a..f4a2f84 100644 --- a/circle/dashboard/templates/dashboard/confirm/node-status.html +++ b/circle/dashboard/templates/dashboard/confirm/node-status.html @@ -26,7 +26,7 @@ {% csrf_token %} {% trans "Cancel" %} - + diff --git a/circle/dashboard/templates/dashboard/group-detail.html b/circle/dashboard/templates/dashboard/group-detail.html index 4ff5e96..22e1350 100644 --- a/circle/dashboard/templates/dashboard/group-detail.html +++ b/circle/dashboard/templates/dashboard/group-detail.html @@ -1,6 +1,7 @@ {% extends "dashboard/base.html" %} {% load crispy_forms_tags %} {% load i18n %} +{% load static %} {% block title-page %}{{ group.name }} | {% trans "group" %}{% endblock %} @@ -8,9 +9,15 @@
    +
    -
    -
    - -
    -{% csrf_token %} -{% crispy group_profile_form %} -
    +
    +
    + {% csrf_token %} + {% crispy group_profile_form %} +
    +
    -
    +

    {% trans "Available objects for this group" %}

    + +
    -

    {% trans "User list"|capfirst %} - {% if perms.auth.add_user %} - {% trans "Create user" %} - {% endif %} -

    -
    {% csrf_token %} - - - - {% for i in users %} - - - - - - {% endfor %} - {% for i in future_users %} - - - - - - {% endfor %} - - - - - -
    {% trans "Who" %}{% trans "Remove" %}
    - - - {% include "dashboard/_display-name.html" with user=i show_org=True %} - - {% trans "remove" %} -
    - - {{ i.org_id }} - - {% trans "remove" %} -
    - {{addmemberform.new_member}} -
    - -
    - -
    -
    +

    + {% trans "User list" %} + {% if perms.auth.add_user %} + + {% trans "Create user" %} + + {% endif %} +

    +
    {% csrf_token %} + + + + {% for i in users %} + + + + + + {% endfor %} + {% for i in future_users %} + + + + + + {% endfor %} + + + + + +
    {% trans "Who" %}{% trans "Remove" %}
    + + + {% include "dashboard/_display-name.html" with user=i show_org=True %} + + + {% trans "remove" %} + +
    + + {{ i.org_id }} + + {% trans "remove" %} +
    + {{addmemberform.new_member}} +
    + +
    + +
    +
    +
    +

    {% trans "Access permissions" %}

    + {% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %} -
    -

    {% trans "Access permissions"|capfirst %}

    -{% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %} -{% if user.is_superuser %} -
    + {% if user.is_superuser %} +
    - - -{{ group_perm_form.media }} + + + {{ group_perm_form.media }} -

    {% trans "Group permissions" %}

    +

    {% trans "Group permissions" %}

    -
    -{% crispy group_perm_form %} -
    +
    + {% crispy group_perm_form %} +
    - - -{% endif %} -
    -
    + + {% endif %} +
    {% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/group-list.html b/circle/dashboard/templates/dashboard/group-list.html index a559c00..d696178 100644 --- a/circle/dashboard/templates/dashboard/group-list.html +++ b/circle/dashboard/templates/dashboard/group-list.html @@ -11,10 +11,10 @@
    -

    Your groups

    +

    {% trans "Groups" %}

    -
    +
    {% render_table table %}
    diff --git a/circle/dashboard/templates/dashboard/index-groups.html b/circle/dashboard/templates/dashboard/index-groups.html index 7be319e..e78c80b 100644 --- a/circle/dashboard/templates/dashboard/index-groups.html +++ b/circle/dashboard/templates/dashboard/index-groups.html @@ -30,7 +30,7 @@
    - {% if more_groups > 0 %} + {% if more_groups > 0 %} {% blocktrans count more=more_groups %} {{ more }} more {% plural %} diff --git a/circle/dashboard/templates/dashboard/index-nodes.html b/circle/dashboard/templates/dashboard/index-nodes.html index ca6667f..8574869 100644 --- a/circle/dashboard/templates/dashboard/index-nodes.html +++ b/circle/dashboard/templates/dashboard/index-nodes.html @@ -1,5 +1,5 @@ {% load i18n %} - - diff --git a/circle/dashboard/templates/dashboard/index.html b/circle/dashboard/templates/dashboard/index.html index db72a3e..130a0c3 100644 --- a/circle/dashboard/templates/dashboard/index.html +++ b/circle/dashboard/templates/dashboard/index.html @@ -36,7 +36,7 @@
    {% endif %} - {% if user.is_superuser %} + {% if perms.vm.view_statistics %}
    {% include "dashboard/index-nodes.html" %}
    diff --git a/circle/dashboard/templates/dashboard/instanceactivity_detail.html b/circle/dashboard/templates/dashboard/instanceactivity_detail.html index adeed94..3733550 100644 --- a/circle/dashboard/templates/dashboard/instanceactivity_detail.html +++ b/circle/dashboard/templates/dashboard/instanceactivity_detail.html @@ -58,6 +58,26 @@
    {% trans "resultant state" %}
    {{object.resultant_state|default:'n/a'}}
    + +
    {% trans "subactivities" %}
    + {% for s in object.children.all %} +
    + + + {{ s.readable_name|get_text:user|capfirst }} – + {% if s.finished %} + {{ s.finished|time:"H:i:s" }} + {% else %} + + {% endif %} + {% if s.has_failed %} +
    {% trans "failed" %}
    + {% endif %} +
    + {% empty %} +
    {% trans "none" %}
    + {% endfor %} +
    diff --git a/circle/dashboard/templates/dashboard/node-add-trait.html b/circle/dashboard/templates/dashboard/node-add-trait.html index fac24bf..38ccbd1 100644 --- a/circle/dashboard/templates/dashboard/node-add-trait.html +++ b/circle/dashboard/templates/dashboard/node-add-trait.html @@ -16,7 +16,7 @@
    -

    {% trans "Add Trait" %}

    +

    {% trans "Add Trait" %}

    {% with form=form %} diff --git a/circle/dashboard/templates/dashboard/node-detail.html b/circle/dashboard/templates/dashboard/node-detail.html index fd35f17..86b9922 100644 --- a/circle/dashboard/templates/dashboard/node-detail.html +++ b/circle/dashboard/templates/dashboard/node-detail.html @@ -7,14 +7,16 @@ {% block content %}
    diff --git a/circle/dashboard/templates/dashboard/node-detail/home.html b/circle/dashboard/templates/dashboard/node-detail/home.html index e13dcfa..cf335c8 100644 --- a/circle/dashboard/templates/dashboard/node-detail/home.html +++ b/circle/dashboard/templates/dashboard/node-detail/home.html @@ -8,24 +8,27 @@ {% for t in node.traits.all %}
    {{ t }} - +
    {% endfor %} {% else %} - {% trans "No trait added!" %} + {% trans "No trait added!" %} {% endif %}
    {% load crispy_forms_tags %} + - - {% csrf_token %} - {% crispy trait_form %} - + {% if request.user.is_superuser %} +
    + {% csrf_token %} + {% crispy trait_form %} +
    + {% endif %}
    diff --git a/circle/dashboard/templates/dashboard/node-detail/resources.html b/circle/dashboard/templates/dashboard/node-detail/resources.html index 6a0c288..9082bad 100644 --- a/circle/dashboard/templates/dashboard/node-detail/resources.html +++ b/circle/dashboard/templates/dashboard/node-detail/resources.html @@ -18,10 +18,12 @@
    {% trans "Host name" %}:
    {{ node.host.hostname }} + {% if request.user.is_superuser %} {% trans "Edit host" %} + {% endif %}
    diff --git a/circle/dashboard/templates/dashboard/node-list/column-vm.html b/circle/dashboard/templates/dashboard/node-list/column-vm.html index e12ecd4..c8d6b0c 100644 --- a/circle/dashboard/templates/dashboard/node-list/column-vm.html +++ b/circle/dashboard/templates/dashboard/node-list/column-vm.html @@ -1,7 +1,7 @@ {% load i18n %} diff --git a/circle/dashboard/templates/dashboard/store/_list-box.html b/circle/dashboard/templates/dashboard/store/_list-box.html index 412717b..e9e4606 100644 --- a/circle/dashboard/templates/dashboard/store/_list-box.html +++ b/circle/dashboard/templates/dashboard/store/_list-box.html @@ -4,23 +4,23 @@
    - {% trans "Upload" %} -
    {% csrf_token %} - +
    {% trans "Browse..." %} - + -
    - - @@ -51,7 +51,7 @@ - @@ -64,7 +64,7 @@
    - {% if current == "/" %}
    - {% trans "Download" %} - {% trans "Remove" %} diff --git a/circle/dashboard/templates/dashboard/template-edit.html b/circle/dashboard/templates/dashboard/template-edit.html index 40c1b86..b41061d 100644 --- a/circle/dashboard/templates/dashboard/template-edit.html +++ b/circle/dashboard/templates/dashboard/template-edit.html @@ -12,7 +12,9 @@
    - {% trans "Back" %} + + {% trans "Back" %} +

    {% trans "Edit template" %}

    @@ -66,6 +68,18 @@
    + {% if is_owner %} +
    +
    + + {% trans "Delete" %} + +

    {% trans "Delete template" %}

    +
    +
    + {% endif %} +

    {% trans "Manage access" %}

    @@ -87,7 +101,13 @@ {% endif %} {% for d in disks %}
  • - {% include "dashboard/_disk-list-element.html" %} + + {{ d.name }} (#{{ d.id }}) - + + {% if long_remove %} {% trans "Remove" %}{% endif %} +
  • {% endfor %} diff --git a/circle/dashboard/templates/dashboard/vm-detail.html b/circle/dashboard/templates/dashboard/vm-detail.html index 60e901e..b8c9fd7 100644 --- a/circle/dashboard/templates/dashboard/vm-detail.html +++ b/circle/dashboard/templates/dashboard/vm-detail.html @@ -8,14 +8,25 @@ {% block content %} {% if instance.is_base %} -
    - {% trans "This is the master vm of your new template" %} -
    - + -
      + + {% trans "This is the master vm of your new template" %} +
      1. {% trans "Modify the virtual machine to suit your needs (optional)" %}
        • {% trans "Change the description" %}
        • @@ -67,7 +78,7 @@
          - + {% for a in activities %} -
          +
          @@ -33,7 +34,8 @@ {% if a.children.count > 0 %}
          {% for s in a.children.all %} -
          +
          {{ s.readable_name|get_text:user|capfirst }} – diff --git a/circle/dashboard/templates/dashboard/vm-detail/home.html b/circle/dashboard/templates/dashboard/vm-detail/home.html index 6ad3508..1586ca6 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/home.html +++ b/circle/dashboard/templates/dashboard/vm-detail/home.html @@ -1,7 +1,7 @@ {% load i18n %}
          -
          +
          {% trans "System" %}:
          {{ instance.system }}
          @@ -52,30 +52,34 @@
          -

          {% trans "Expiration" %} {% if instance.is_expiring %}{% endif %} - - {% with op=op.renew %}{% if op %} - - - {{op.name}} - {% endif %}{% endwith %} - -

          -
          -
          {% trans "Suspended at:" %}
          -
          - - {{ instance.time_of_suspend|timeuntil }} +
          +

          + {% trans "Expiration" %} + {% if instance.is_expiring %}{% endif %} + + {% with op=op.renew %}{% if op %} + + + {{op.name}} + {% endif %}{% endwith %} -

          -
          {% trans "Destroyed at:" %}
          -
          - - {{ instance.time_of_delete|timeuntil }} - -
          -
          + +
          +
          {% trans "Suspended at:" %}
          +
          + + {{ instance.time_of_suspend|timeuntil }} + +
          +
          {% trans "Destroyed at:" %}
          +
          + + {{ instance.time_of_delete|timeuntil }} + +
          +
          +
          {% trans "Tags" %}
          diff --git a/circle/dashboard/templates/dashboard/vm-detail/resources.html b/circle/dashboard/templates/dashboard/vm-detail/resources.html index e14cf6b..51e6d48 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/resources.html +++ b/circle/dashboard/templates/dashboard/vm-detail/resources.html @@ -18,34 +18,28 @@ {% endif %} -
          - -
          -
          -

          - {% trans "Disks" %} -
          -
          - {% include "dashboard/vm-detail/_disk-operations.html" %} -
          +
          +

          + {% trans "Disks" %} +
          +
          + {% include "dashboard/vm-detail/_disk-operations.html" %}
          -

          - -
          - - {% if not instance.disks.all %} - {% trans "No disks are added!" %} - {% endif %} - {% for d in instance.disks.all %} -

          - {% with long_remove=True %} - {% include "dashboard/_disk-list-element.html" %} - {% endwith %} -

          - {% endfor %} -
          +
          +

          + + {% if not instance.disks.all %} + {% trans "No disks are added." %} + {% endif %} + {% for d in instance.disks.all %} +

          + {% with long_remove=True %} + {% include "dashboard/_disk-list-element.html" %} + {% endwith %} +

          + {% endfor %}
          {% if user.is_superuser %} diff --git a/circle/dashboard/templates/dashboard/vm-list.html b/circle/dashboard/templates/dashboard/vm-list.html index 1501e90..51c0eb9 100644 --- a/circle/dashboard/templates/dashboard/vm-list.html +++ b/circle/dashboard/templates/dashboard/vm-list.html @@ -73,6 +73,10 @@ {% trans "Lease" as t %} {% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %} + + {% trans "Memory" as t %} + {% include "dashboard/vm-list/header-link.html" with name=t sort="ram_size" %} + {% if user.is_superuser %} {% trans "IP address" as t %} @@ -87,7 +91,9 @@ {% for i in object_list %}
          {{i.pk}}
          - {{ i.name }} + + {{ i.name }} + - {{ i.lease.name }} + + {{ i.lease.name }} + + + + {{ i.ram_size }} MiB {% if user.is_superuser %} diff --git a/circle/dashboard/tests/test_mockedviews.py b/circle/dashboard/tests/test_mockedviews.py index 4a28660..7d22bd4 100644 --- a/circle/dashboard/tests/test_mockedviews.py +++ b/circle/dashboard/tests/test_mockedviews.py @@ -34,6 +34,13 @@ from ..views import AclUpdateView from .. import views +class QuerySet(list): + model = MagicMock() + + def get(self, *args, **kwargs): + return self.pop() + + class ViewUserTestCase(unittest.TestCase): def test_404(self): @@ -145,58 +152,66 @@ class VmOperationViewTestCase(unittest.TestCase): view.as_view()(request, pk=1234).render() def test_migrate(self): - request = FakeRequestFactory(POST={'node': 1}, superuser=True) + request = FakeRequestFactory( + POST={'to_node': 1, 'live_migration': True}, superuser=True) view = vm_ops['migrate'] + node = MagicMock(pk=1, name='node1') with patch.object(view, 'get_object') as go, \ patch('dashboard.views.util.messages') as msg, \ - patch('dashboard.views.vm.get_object_or_404') as go4: + patch.object(view, 'get_form_kwargs') as form_kwargs: 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 + form_kwargs.return_value = { + 'default': 100, 'choices': QuerySet([node])} go.return_value = inst - go4.return_value = MagicMock() assert view.as_view()(request, pk=1234)['location'] assert not msg.error.called - assert go4.called + inst.migrate.async.assert_called_once_with( + to_node=node, live_migration=True, user=request.user) def test_migrate_failed(self): - request = FakeRequestFactory(POST={'node': 1}, superuser=True) + request = FakeRequestFactory(POST={'to_node': 1}, superuser=True) view = vm_ops['migrate'] + node = MagicMock(pk=1, name='node1') with patch.object(view, 'get_object') as go, \ patch('dashboard.views.util.messages') as msg, \ - patch('dashboard.views.vm.get_object_or_404') as go4: + patch.object(view, 'get_form_kwargs') as form_kwargs: 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 + form_kwargs.return_value = { + 'default': 100, 'choices': QuerySet([node])} go.return_value = inst - go4.return_value = MagicMock() assert view.as_view()(request, pk=1234)['location'] + assert inst.migrate.async.called assert msg.error.called - assert go4.called def test_migrate_wo_permission(self): - request = FakeRequestFactory(POST={'node': 1}, superuser=False) + request = FakeRequestFactory(POST={'to_node': 1}, superuser=False) view = vm_ops['migrate'] + node = MagicMock(pk=1, name='node1') with patch.object(view, 'get_object') as go, \ - patch('dashboard.views.vm.get_object_or_404') as go4: + patch.object(view, 'get_form_kwargs') as form_kwargs: 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 + form_kwargs.return_value = { + 'default': 100, 'choices': QuerySet([node])} go.return_value = inst - go4.return_value = MagicMock() with self.assertRaises(PermissionDenied): assert view.as_view()(request, pk=1234)['location'] - assert go4.called + assert not inst.migrate.async.called def test_migrate_template(self): """check if GET dialog's template can be rendered""" @@ -219,6 +234,7 @@ class VmOperationViewTestCase(unittest.TestCase): with patch.object(view, 'get_object') as go, \ patch('dashboard.views.util.messages') as msg: inst = MagicMock(spec=Instance) + inst.name = "asd" inst._meta.object_name = "Instance" inst.save_as_template = Instance._ops['save_as_template'](inst) inst.save_as_template.async = MagicMock() @@ -235,6 +251,7 @@ class VmOperationViewTestCase(unittest.TestCase): with patch.object(view, 'get_object') as go, \ patch('dashboard.views.util.messages') as msg: inst = MagicMock(spec=Instance) + inst.name = "asd" inst._meta.object_name = "Instance" inst.save_as_template = Instance._ops['save_as_template'](inst) inst.save_as_template.async = MagicMock() @@ -301,7 +318,7 @@ class VmMassOperationViewTestCase(unittest.TestCase): view.as_view()(request, pk=1234).render() def test_migrate(self): - request = FakeRequestFactory(POST={'node': 1}, superuser=True) + request = FakeRequestFactory(POST={'to_node': 1}, superuser=True) view = vm_mass_ops['migrate'] with patch.object(view, 'get_object') as go, \ @@ -318,7 +335,7 @@ class VmMassOperationViewTestCase(unittest.TestCase): assert not msg2.error.called def test_migrate_failed(self): - request = FakeRequestFactory(POST={'node': 1}, superuser=True) + request = FakeRequestFactory(POST={'to_node': 1}, superuser=True) view = vm_mass_ops['migrate'] with patch.object(view, 'get_object') as go, \ @@ -334,7 +351,7 @@ class VmMassOperationViewTestCase(unittest.TestCase): assert msg.error.called def test_migrate_wo_permission(self): - request = FakeRequestFactory(POST={'node': 1}, superuser=False) + request = FakeRequestFactory(POST={'to_node': 1}, superuser=False) view = vm_mass_ops['migrate'] with patch.object(view, 'get_object') as go: diff --git a/circle/dashboard/tests/test_views.py b/circle/dashboard/tests/test_views.py index ac34595..b157176 100644 --- a/circle/dashboard/tests/test_views.py +++ b/circle/dashboard/tests/test_views.py @@ -299,7 +299,7 @@ class VmDetailTest(LoginMixin, TestCase): leases = Lease.objects.count() response = c.post("/dashboard/lease/delete/1/") # redirect to the login page - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 403) self.assertEqual(leases, Lease.objects.count()) def test_notification_read(self): @@ -512,20 +512,20 @@ class VmDetailTest(LoginMixin, TestCase): self.login(c, "user2") with patch.object(Instance, 'select_node', return_value=None), \ patch.object(WakeUpOperation, 'async') as new_wake_up, \ - patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \ patch.object(Instance.WrongStateError, 'send_message') as wro: inst = Instance.objects.get(pk=1) new_wake_up.side_effect = inst.wake_up + inst._wake_up_vm = Mock() inst.get_remote_queue_name = Mock(return_value='test') inst.status = 'SUSPENDED' inst.set_level(self.u2, 'owner') with patch('dashboard.views.messages') as msg: response = c.post("/dashboard/vm/1/op/wake_up/") assert not msg.error.called + assert inst._wake_up_vm.called self.assertEqual(response.status_code, 302) self.assertEqual(inst.status, 'RUNNING') assert new_wake_up.called - assert wuaa.called assert not wro.called def test_unpermitted_wake_up(self): @@ -637,7 +637,7 @@ class NodeDetailTest(LoginMixin, TestCase): c = Client() self.login(c, 'user1') response = c.get('/dashboard/node/25555/') - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 403) def test_anon_node_page(self): c = Client() @@ -667,7 +667,7 @@ class NodeDetailTest(LoginMixin, TestCase): node = Node.objects.get(pk=1) old_name = node.name response = c.post("/dashboard/node/1/", {'new_name': 'test1235'}) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 403) self.assertEqual(Node.objects.get(pk=1).name, old_name) def test_permitted_set_name(self): @@ -721,7 +721,7 @@ class NodeDetailTest(LoginMixin, TestCase): c = Client() self.login(c, "user2") response = c.post("/dashboard/node/1/", {'to_remove': traitid}) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 403) self.assertEqual(Node.objects.get(pk=1).traits.count(), trait_count) def test_permitted_remove_trait(self): diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index a104389..7d59f40 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -45,6 +45,7 @@ from .views import ( VmTraitsUpdate, VmRawDataUpdate, GroupPermissionsView, LeaseAclUpdateView, + toggle_template_tutorial, ClientCheck, TokenLogin, VmGraphView, NodeGraphView, NodeListGraphView, ) @@ -99,6 +100,8 @@ urlpatterns = patterns( name='dashboard.views.vm-traits'), url(r'^vm/(?P\d+)/raw_data/$', VmRawDataUpdate.as_view(), name='dashboard.views.vm-raw-data'), + url(r'^vm/(?P\d+)/toggle_tutorial/$', toggle_template_tutorial, + name='dashboard.views.vm-toggle-tutorial'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/(?P\d+)/$', NodeDetailView.as_view(), diff --git a/circle/dashboard/views/graph.py b/circle/dashboard/views/graph.py index fdd9699..472ea1e 100644 --- a/circle/dashboard/views/graph.py +++ b/circle/dashboard/views/graph.py @@ -26,7 +26,7 @@ from django.http import HttpResponse, Http404 from django.utils.translation import ugettext_lazy as _ from django.views.generic import View -from braces.views import LoginRequiredMixin, SuperuserRequiredMixin +from braces.views import LoginRequiredMixin from vm.models import Instance, Node @@ -142,22 +142,28 @@ class VmGraphView(GraphViewBase): base = VmMetric -class NodeGraphView(SuperuserRequiredMixin, GraphViewBase): +class NodeGraphView(GraphViewBase): model = Node base = NodeMetric def get_object(self, request, pk): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() return self.model.objects.get(id=pk) -class NodeListGraphView(SuperuserRequiredMixin, GraphViewBase): +class NodeListGraphView(GraphViewBase): model = Node base = Metric def get_object(self, request, pk): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() return Node.objects.filter(enabled=True) def get(self, request, metric, time, *args, **kwargs): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() return super(NodeListGraphView, self).get(request, None, metric, time) diff --git a/circle/dashboard/views/group.py b/circle/dashboard/views/group.py index c429f7e..d8fe827 100644 --- a/circle/dashboard/views/group.py +++ b/circle/dashboard/views/group.py @@ -39,6 +39,7 @@ from ..forms import ( GroupCreateForm, GroupProfileUpdateForm, ) from ..models import FutureMember, GroupProfile +from vm.models import Instance, InstanceTemplate from ..tables import GroupListTable from .util import CheckedDetailView, AclUpdateView, search_user, saml_available @@ -100,6 +101,15 @@ class GroupDetailView(CheckedDetailView): context['group_profile_form'] = GroupProfileUpdate.get_form_object( self.request, self.object.profile) + context.update({ + 'group_objects': GroupProfile.get_objects_with_group_level( + "operator", self.get_object()), + 'vm_objects': Instance.get_objects_with_group_level( + "user", self.get_object()), + 'template_objects': InstanceTemplate.get_objects_with_group_level( + "user", self.get_object()), + }) + if self.request.user.is_superuser: context['group_perm_form'] = GroupPermissionForm( instance=self.object) diff --git a/circle/dashboard/views/index.py b/circle/dashboard/views/index.py index 9f7bba0..bd4839a 100644 --- a/circle/dashboard/views/index.py +++ b/circle/dashboard/views/index.py @@ -62,7 +62,7 @@ class IndexView(LoginRequiredMixin, TemplateView): }) # nodes - if user.is_superuser: + if user.has_perm('vm.view_statistics'): nodes = Node.objects.all() context.update({ 'nodes': nodes[:5], diff --git a/circle/dashboard/views/node.py b/circle/dashboard/views/node.py index 2f44962..1d2293c 100644 --- a/circle/dashboard/views/node.py +++ b/circle/dashboard/views/node.py @@ -75,13 +75,18 @@ node_ops = OrderedDict([ ]) -class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, +class NodeDetailView(LoginRequiredMixin, GraphMixin, DetailView): template_name = "dashboard/node-detail.html" model = Node form = None form_class = TraitForm + def get(self, *args, **kwargs): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() + return super(NodeDetailView, self).get(*args, **kwargs) + def get_context_data(self, form=None, **kwargs): if form is None: form = self.form_class() @@ -98,6 +103,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, return context def post(self, request, *args, **kwargs): + if not request.user.is_superuser: + raise PermissionDenied() if request.POST.get('new_name'): return self.__set_name(request) if request.POST.get('to_remove'): @@ -145,13 +152,14 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, return redirect(self.object.get_absolute_url()) -class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, - GraphMixin, SingleTableView): +class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView): template_name = "dashboard/node-list.html" table_class = NodeListTable table_pagination = False def get(self, *args, **kwargs): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() if self.request.is_ajax(): nodes = Node.objects.all() nodes = [{ diff --git a/circle/dashboard/views/store.py b/circle/dashboard/views/store.py index a7debdc..d7acf3c 100644 --- a/circle/dashboard/views/store.py +++ b/circle/dashboard/views/store.py @@ -23,6 +23,7 @@ from os.path import join, normpath, dirname, basename from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.template.defaultfilters import urlencode from django.core.cache import get_cache from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse @@ -55,7 +56,7 @@ class StoreList(LoginRequiredMixin, TemplateView): context['current'] = directory context['next_url'] = "%s%s?directory=%s" % ( settings.DJANGO_URL.rstrip("/"), - reverse("dashboard.views.store-list"), directory) + reverse("dashboard.views.store-list"), urlencode(directory)) return context def get(self, *args, **kwargs): @@ -112,7 +113,7 @@ def store_upload(request): next_url = "%s%s?directory=%s" % ( settings.DJANGO_URL.rstrip("/"), - reverse("dashboard.views.store-list"), directory) + reverse("dashboard.views.store-list"), urlencode(directory)) return render(request, "dashboard/store/upload.html", {'directory': directory, 'action': action, @@ -168,7 +169,7 @@ class StoreRemove(LoginRequiredMixin, TemplateView): return redirect("%s?directory=%s" % ( reverse("dashboard.views.store-list"), - dirname(dirname(path)), + urlencode(dirname(dirname(path))), )) @@ -185,7 +186,7 @@ def store_new_directory(request): name, path, unicode(request.user)) messages.error(request, _("Unable to create folder.")) return redirect("%s?directory=%s" % ( - reverse("dashboard.views.store-list"), path)) + reverse("dashboard.views.store-list"), urlencode(path))) @require_POST diff --git a/circle/dashboard/views/template.py b/circle/dashboard/views/template.py index aac295b..a7454dc 100644 --- a/circle/dashboard/views/template.py +++ b/circle/dashboard/views/template.py @@ -32,11 +32,12 @@ from django.views.generic import ( ) from braces.views import ( - LoginRequiredMixin, PermissionRequiredMixin, SuperuserRequiredMixin, + LoginRequiredMixin, PermissionRequiredMixin, ) from django_tables2 import SingleTableView from vm.models import InstanceTemplate, InterfaceTemplate, Instance, Lease +from storage.models import Disk from ..forms import ( TemplateForm, TemplateListSearchForm, AclUserOrGroupAddForm, LeaseForm, @@ -239,6 +240,16 @@ class TemplateDelete(LoginRequiredMixin, DeleteView): else: return ['dashboard/confirm/base-delete.html'] + def get(self, request, *args, **kwargs): + if not self.get_object().has_level(request.user, "owner"): + message = _("Only the owners can delete the selected template.") + if request.is_ajax(): + raise PermissionDenied() + else: + messages.warning(request, message) + return redirect(self.get_success_url()) + return super(TemplateDelete, self).get(request, *args, **kwargs) + def delete(self, request, *args, **kwargs): object = self.get_object() if not object.has_level(request.user, 'owner'): @@ -319,6 +330,57 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): return kwargs +class DiskRemoveView(DeleteView): + model = Disk + + def get_queryset(self): + qs = super(DiskRemoveView, self).get_queryset() + return qs.exclude(template_set=None) + + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/confirm/ajax-delete.html'] + else: + return ['dashboard/confirm/base-delete.html'] + + def get_context_data(self, **kwargs): + context = super(DiskRemoveView, self).get_context_data(**kwargs) + disk = self.get_object() + template = disk.template_set.get() + if not template.has_level(self.request.user, 'owner'): + raise PermissionDenied() + context['title'] = _("Disk remove confirmation") + context['text'] = _("Are you sure you want to remove " + "%(disk)s from " + "%(app)s?" % {'disk': disk, + 'app': template} + ) + return context + + def delete(self, request, *args, **kwargs): + disk = self.get_object() + template = disk.template_set.get() + + if not template.has_level(request.user, 'owner'): + raise PermissionDenied() + + template.remove_disk(disk=disk, user=request.user) + disk.destroy() + + next_url = request.POST.get("next") + success_url = next_url if next_url else template.get_absolute_url() + success_message = _("Disk successfully removed.") + + if request.is_ajax(): + return HttpResponse( + json.dumps({'message': success_message}), + content_type="application/json", + ) + else: + messages.success(request, success_message) + return HttpResponseRedirect("%s#resources" % success_url) + + class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView): model = Lease @@ -330,13 +392,17 @@ class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, def get_success_url(self): return reverse_lazy("dashboard.views.template-list") + def form_valid(self, form): + retval = super(LeaseCreate, self).form_valid(form) + self.object.set_level(self.request.user, "owner") + return retval + class LeaseAclUpdateView(AclUpdateView): model = Lease -class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, - SuccessMessageMixin, UpdateView): +class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = Lease form_class = LeaseForm template_name = "dashboard/lease-edit.html" @@ -352,8 +418,21 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, def get_success_url(self): return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs) + def get(self, request, *args, **kwargs): + if not self.get_object().has_level(request.user, "owner"): + message = _("Only the owners can modify the selected lease.") + messages.warning(request, message) + return redirect(reverse_lazy("dashboard.views.template-list")) + return super(LeaseDetail, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + if not self.get_object().has_level(request.user, "owner"): + raise PermissionDenied() + + return super(LeaseDetail, self).post(request, *args, **kwargs) -class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): + +class LeaseDelete(LoginRequiredMixin, DeleteView): model = Lease def get_success_url(self): @@ -379,10 +458,22 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): c['disable_submit'] = True return c + def get(self, request, *args, **kwargs): + if not self.get_object().has_level(request.user, "owner"): + message = _("Only the owners can delete the selected lease.") + if request.is_ajax(): + raise PermissionDenied() + else: + messages.warning(request, message) + return redirect(self.get_success_url()) + return super(LeaseDelete, self).get(request, *args, **kwargs) + def delete(self, request, *args, **kwargs): object = self.get_object() - if (object.instancetemplate_set.count() > 0): + if not object.has_level(request.user, "owner"): + raise PermissionDenied() + if object.instancetemplate_set.count() > 0: raise SuspiciousOperation() object.delete() diff --git a/circle/dashboard/views/util.py b/circle/dashboard/views/util.py index 26f4622..c870c4a 100644 --- a/circle/dashboard/views/util.py +++ b/circle/dashboard/views/util.py @@ -184,7 +184,7 @@ class OperationView(RedirectToLoginMixin, DetailView): @classmethod def get_urlname(cls): - return 'dashboard.vm.op.%s' % cls.op + return 'dashboard.%s.op.%s' % (cls.model._meta.model_name, cls.op) @classmethod def get_instance_url(cls, pk, key=None, *args, **kwargs): diff --git a/circle/dashboard/views/vm.py b/circle/dashboard/views/vm.py index 31b14d9..60fe219 100644 --- a/circle/dashboard/views/vm.py +++ b/circle/dashboard/views/vm.py @@ -24,6 +24,7 @@ from os import getenv from django.conf import settings from django.contrib import messages from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required from django.core import signing from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.urlresolvers import reverse, reverse_lazy @@ -45,9 +46,10 @@ from common.models import ( create_readable, HumanReadableException, fetch_human_exception, ) from firewall.models import Vlan, Host, Rule +from manager.scheduler import SchedulerError from storage.models import Disk from vm.models import ( - Instance, instance_activity, InstanceActivity, Node, Lease, + Instance, InstanceActivity, Node, Lease, InstanceTemplate, InterfaceTemplate, Interface, ) from .util import ( @@ -58,7 +60,8 @@ from ..forms import ( AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm, VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, - TransferOwnershipForm, VmDiskResizeForm, RedeployForm, + TransferOwnershipForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, + VmMigrateForm, VmDeployForm, ) from ..models import Favourite, Profile @@ -76,10 +79,10 @@ class VmDetailVncTokenView(CheckedDetailView): if not request.user.has_perm('vm.access_console'): raise PermissionDenied() if self.object.node: - with instance_activity( - code_suffix='console-accessed', instance=self.object, - user=request.user, readable_name=ugettext_noop( - "console access"), concurrency_check=False): + with self.object.activity( + code_suffix='console-accessed', user=request.user, + readable_name=ugettext_noop("console access"), + concurrency_check=False): port = self.object.vnc_port host = str(self.object.node.host.ipv4) value = signing.dumps({'host': host, 'port': port}, @@ -100,13 +103,16 @@ class VmDetailView(GraphMixin, CheckedDetailView): is_operator = instance.has_level(user, "operator") is_owner = instance.has_level(user, "owner") ops = get_operations(instance, user) + hide_tutorial = self.request.COOKIES.get( + "hide_tutorial_for_%s" % instance.pk) == "True" context.update({ 'graphite_enabled': settings.GRAPHITE_URL is not None, 'vnc_url': reverse_lazy("dashboard.views.detail-vnc", kwargs={'pk': self.object.pk}), 'ops': ops, 'op': {i.op: i for i in ops}, - 'connect_commands': user.profile.get_connect_commands(instance) + 'connect_commands': user.profile.get_connect_commands(instance), + 'hide_tutorial': hide_tutorial, }) # activity data @@ -369,11 +375,9 @@ class VmAddInterfaceView(FormOperationMixin, VmOperationView): return val -class VmDiskResizeView(FormOperationMixin, VmOperationView): - - op = 'resize_disk' - form_class = VmDiskResizeForm +class VmDiskModifyView(FormOperationMixin, VmOperationView): show_in_toolbar = False + with_reload = True icon = 'arrows-alt' effect = "success" @@ -388,7 +392,7 @@ class VmDiskResizeView(FormOperationMixin, VmOperationView): else: default = None - val = super(VmDiskResizeView, self).get_form_kwargs() + val = super(VmDiskModifyView, self).get_form_kwargs() val.update({'choices': choices, 'default': default}) return val @@ -401,6 +405,14 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView): icon = 'hdd-o' effect = "success" is_disk_operation = True + with_reload = True + + def get_form_kwargs(self): + op = self.get_op() + val = super(VmCreateDiskView, self).get_form_kwargs() + num = op.instance.disks.count() + 1 + val['default'] = "%s %d" % (op.instance.name, num) + return val class VmDownloadDiskView(FormOperationMixin, VmOperationView): @@ -411,29 +423,31 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView): icon = 'download' effect = "success" is_disk_operation = True + with_reload = True -class VmMigrateView(VmOperationView): +class VmMigrateView(FormOperationMixin, VmOperationView): op = 'migrate' icon = 'truck' effect = 'info' template_name = 'dashboard/_vm-migrate.html' + form_class = VmMigrateForm - def get_context_data(self, **kwargs): - ctx = super(VmMigrateView, self).get_context_data(**kwargs) - ctx['nodes'] = [n for n in Node.objects.filter(enabled=True) - if n.online] - return ctx + def get_form_kwargs(self): + online = (n.pk for n in Node.objects.filter(enabled=True) if n.online) + choices = Node.objects.filter(pk__in=online) + default = None + inst = self.get_object() + try: + if isinstance(inst, Instance): + default = inst.select_node() + except SchedulerError: + logger.exception("scheduler error:") - def post(self, request, extra=None, *args, **kwargs): - if extra is None: - extra = {} - node = self.request.POST.get("node") - if node: - node = get_object_or_404(Node, pk=node) - extra["to_node"] = node - return super(VmMigrateView, self).post(request, extra, *args, **kwargs) + val = super(VmMigrateView, self).get_form_kwargs() + val.update({'choices': choices, 'default': default}) + return val class VmSaveView(FormOperationMixin, VmOperationView): @@ -443,6 +457,16 @@ class VmSaveView(FormOperationMixin, VmOperationView): effect = 'info' form_class = VmSaveForm + def get_form_kwargs(self): + op = self.get_op() + val = super(VmSaveView, self).get_form_kwargs() + val['default'] = op._rename(op.instance.name) + obj = self.get_object() + if obj.template and obj.template.has_level( + self.request.user, "owner"): + val['clone'] = True + return val + class VmResourcesChangeView(VmOperationView): op = 'resources_change' @@ -589,7 +613,6 @@ class VmStateChangeView(FormOperationMixin, VmOperationView): op = 'emergency_change_state' icon = 'legal' effect = 'danger' - show_in_toolbar = True form_class = VmStateChangeForm wait_for_result = 0.5 @@ -612,9 +635,23 @@ class RedeployView(FormOperationMixin, VmOperationView): wait_for_result = 0.5 +class VmDeployView(FormOperationMixin, VmOperationView): + op = 'deploy' + icon = 'play' + effect = 'success' + form_class = VmDeployForm + + def get_form_kwargs(self): + kwargs = super(VmDeployView, self).get_form_kwargs() + if self.request.user.is_superuser: + online = (n.pk for n in + Node.objects.filter(enabled=True) if n.online) + kwargs['choices'] = Node.objects.filter(pk__in=online) + return kwargs + + vm_ops = OrderedDict([ - ('deploy', VmOperationView.factory( - op='deploy', icon='play', effect='success')), + ('deploy', VmDeployView), ('wake_up', VmOperationView.factory( op='wake_up', icon='sun-o', effect='success')), ('sleep', VmOperationView.factory( @@ -629,7 +666,7 @@ vm_ops = OrderedDict([ ('shutdown', VmOperationView.factory( op='shutdown', icon='power-off', effect='warning')), ('shut_off', VmOperationView.factory( - op='shut_off', icon='ban', effect='warning')), + op='shut_off', icon='plug', effect='warning')), ('recover', VmOperationView.factory( op='recover', icon='medkit', effect='warning')), ('nostate', VmStateChangeView), @@ -639,7 +676,12 @@ vm_ops = OrderedDict([ op='destroy', icon='times', effect='danger')), ('create_disk', VmCreateDiskView), ('download_disk', VmDownloadDiskView), - ('resize_disk', VmDiskResizeView), + ('resize_disk', VmDiskModifyView.factory( + op='resize_disk', form_class=VmDiskResizeForm, + icon='arrows-alt', effect="warning")), + ('remove_disk', VmDiskModifyView.factory( + op='remove_disk', form_class=VmDiskRemoveForm, + icon='times', effect="danger")), ('add_interface', VmAddInterfaceView), ('renew', VmRenewView), ('resources_change', VmResourcesChangeView), @@ -741,6 +783,12 @@ class MassOperationView(OperationView): self.check_auth() if extra is None: extra = {} + + if hasattr(self, 'form_class'): + form = self.form_class(self.request.POST, **self.get_form_kwargs()) + if form.is_valid(): + extra.update(form.cleaned_data) + self._call_operations(extra) if request.is_ajax(): store = messages.get_messages(request) @@ -779,6 +827,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): allowed_filters = { 'name': "name__icontains", 'node': "node__name__icontains", + 'node_exact': "node__name", 'status': "status__iexact", 'tags[]': "tags__name__in", 'tags': "tags__name__in", # for search string @@ -864,10 +913,9 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): in [i.name for i in Instance._meta.fields] + ["pk"]): queryset = queryset.order_by(sort) - return queryset.filter( - **self.get_queryset_filters()).prefetch_related( - "owner", "node", "owner__profile", "interface_set", "lease", - "interface_set__host").distinct() + return queryset.filter(**self.get_queryset_filters()).prefetch_related( + "owner", "node", "owner__profile", "interface_set", "lease", + "interface_set__host").distinct() class VmCreate(LoginRequiredMixin, TemplateView): @@ -1103,51 +1151,6 @@ class InstanceActivityDetail(CheckedDetailView): return ctx -class DiskRemoveView(DeleteView): - model = Disk - - def get_template_names(self): - if self.request.is_ajax(): - return ['dashboard/confirm/ajax-delete.html'] - else: - return ['dashboard/confirm/base-delete.html'] - - def get_context_data(self, **kwargs): - context = super(DiskRemoveView, self).get_context_data(**kwargs) - disk = self.get_object() - app = disk.get_appliance() - context['title'] = _("Disk remove confirmation") - context['text'] = _("Are you sure you want to remove " - "%(disk)s from " - "%(app)s?" % {'disk': disk, - 'app': app} - ) - return context - - def delete(self, request, *args, **kwargs): - disk = self.get_object() - app = disk.get_appliance() - - if not app.has_level(request.user, 'owner'): - raise PermissionDenied() - - app.remove_disk(disk=disk, user=request.user) - disk.destroy() - - next_url = request.POST.get("next") - success_url = next_url if next_url else app.get_absolute_url() - success_message = _("Disk successfully removed.") - - if request.is_ajax(): - return HttpResponse( - json.dumps({'message': success_message}), - content_type="application/json", - ) - else: - messages.success(request, success_message) - return HttpResponseRedirect("%s#resources" % success_url) - - @require_GET def get_disk_download_status(request, pk): disk = Disk.objects.get(pk=pk) @@ -1395,9 +1398,8 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): instance, owner = self.get_instance(key, request.user) old = instance.owner - with instance_activity(code_suffix='ownership-transferred', - concurrency_check=False, - instance=instance, user=request.user): + with instance.activity(code_suffix='ownership-transferred', + concurrency_check=False, user=request.user): instance.owner = request.user instance.clean() instance.save() @@ -1437,3 +1439,13 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): unicode(user), user.pk, new_owner, key) raise PermissionDenied() return (instance, new_owner) + + +@login_required +def toggle_template_tutorial(request, pk): + hidden = request.POST.get("hidden", "").lower() == "true" + instance = get_object_or_404(Instance, pk=pk) + response = HttpResponseRedirect(instance.get_absolute_url()) + response.set_cookie( # for a week + "hide_tutorial_for_%s" % pk, hidden, 7 * 24 * 60 * 60) + return response diff --git a/circle/fabfile.py b/circle/fabfile.py index 0ec424d..e09a256 100755 --- a/circle/fabfile.py +++ b/circle/fabfile.py @@ -94,6 +94,10 @@ def make_messages(): def test(test=""): "Run portal tests" with _workon("circle"), cd("~/circle/circle"): + if test == "f": + test = "--failed" + else: + test += " --with-id" run("./manage.py test --settings=circle.settings.test %s" % test) diff --git a/circle/firewall/migrations/0052_auto__chg_field_record_address.py b/circle/firewall/migrations/0052_auto__chg_field_record_address.py new file mode 100644 index 0000000..e911ec8 --- /dev/null +++ b/circle/firewall/migrations/0052_auto__chg_field_record_address.py @@ -0,0 +1,201 @@ +# -*- 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): + + # Changing field 'Record.address' + db.alter_column(u'firewall_record', 'address', self.gf('django.db.models.fields.CharField')(max_length=400)) + + def backwards(self, orm): + + # Changing field 'Record.address' + db.alter_column(u'firewall_record', 'address', self.gf('django.db.models.fields.CharField')(max_length=200)) + + 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'firewall.blacklistitem': { + 'Meta': {'object_name': 'BlacklistItem'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipv4': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}), + 'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'reason': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'snort_message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'default': "'tempban'", 'max_length': '10'}) + }, + 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.ethernetdevice': { + 'Meta': {'object_name': 'EthernetDevice'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', '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'}), + 'switch_port': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ethernet_devices'", 'to': u"orm['firewall.SwitchPort']"}) + }, + u'firewall.firewall': { + 'Meta': {'object_name': 'Firewall'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}) + }, + 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.record': { + 'Meta': {'ordering': "('domain', 'name')", 'object_name': 'Record'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '400'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', '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', 'null': 'True', 'blank': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '6'}) + }, + u'firewall.rule': { + 'Meta': {'ordering': "('direction', 'proto', 'sport', 'dport', 'nat_external_port', 'host')", 'object_name': 'Rule'}, + 'action': ('django.db.models.fields.CharField', [], {'default': "'drop'", 'max_length': '10'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'direction': ('django.db.models.fields.CharField', [], {'max_length': '3'}), + 'dport': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'extra': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'firewall': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Firewall']"}), + 'foreign_network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ForeignRules'", 'to': u"orm['firewall.VlanGroup']"}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Host']"}), + 'hostgroup': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'nat': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'nat_external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'nat_external_port': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'proto': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'sport': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'vlan': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Vlan']"}), + 'vlangroup': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.VlanGroup']"}), + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '30000'}) + }, + u'firewall.switchport': { + 'Meta': {'object_name': 'SwitchPort'}, + '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'}), + 'tagged_vlans': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tagged_ports'", 'null': 'True', 'to': u"orm['firewall.VlanGroup']"}), + 'untagged_vlan': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'untagged_ports'", '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'firewall.vlangroup': { + 'Meta': {'object_name': 'VlanGroup'}, + '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'}), + 'vlans': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['firewall'] \ No newline at end of file diff --git a/circle/firewall/models.py b/circle/firewall/models.py index 882ba3f..cfa4a99 100644 --- a/circle/firewall/models.py +++ b/circle/firewall/models.py @@ -874,7 +874,7 @@ class Record(models.Model): verbose_name=_('host')) type = models.CharField(max_length=6, choices=CHOICES_type, verbose_name=_('type')) - address = models.CharField(max_length=200, + address = models.CharField(max_length=400, verbose_name=_('address')) ttl = models.IntegerField(default=600, verbose_name=_('ttl')) owner = models.ForeignKey(User, verbose_name=_('owner')) diff --git a/circle/firewall/tasks/local_tasks.py b/circle/firewall/tasks/local_tasks.py index 3085aca..5731a82 100644 --- a/circle/firewall/tasks/local_tasks.py +++ b/circle/firewall/tasks/local_tasks.py @@ -29,26 +29,24 @@ settings = django.conf.settings.FIREWALL_SETTINGS logger = getLogger(__name__) -def _apply_once(name, queues, task, data): +def _apply_once(name, tasks, queues, task, data): """Reload given networking component if needed. """ - lockname = "%s_lock" % name - if not cache.get(lockname): + if name not in tasks: return - cache.delete(lockname) data = data() for queue in queues: try: - task.apply_async(args=data, queue=queue, expires=60).get(timeout=5) + task.apply_async(args=data, queue=queue, expires=60).get(timeout=2) logger.info("%s configuration is reloaded. (queue: %s)", name, queue) except TimeoutError as e: - logger.critical('%s (queue: %s)', e, queue) + logger.critical('%s (queue: %s, task: %s)', e, queue, name) except: - logger.critical('Unhandled exception: queue: %s data: %s', - queue, data, exc_info=True) + logger.critical('Unhandled exception: queue: %s data: %s task: %s', + queue, data, name, exc_info=True) def get_firewall_queues(): @@ -68,19 +66,28 @@ def reloadtask_worker(): from remote_tasks import (reload_dns, reload_dhcp, reload_firewall, reload_firewall_vlan, reload_blacklist) + tasks = [] + for i in ('dns', 'dhcp', 'firewall', 'firewall_vlan', 'blacklist'): + lockname = "%s_lock" % i + if cache.get(lockname): + tasks.append(i) + cache.delete(lockname) + + logger.info("reloadtask_worker: Reload %s", ", ".join(tasks)) + firewall_queues = get_firewall_queues() dns_queues = [("%s.dns" % i) for i in settings.get('dns_queues', [gethostname()])] - _apply_once('dns', dns_queues, reload_dns, + _apply_once('dns', tasks, dns_queues, reload_dns, lambda: (dns(), )) - _apply_once('dhcp', firewall_queues, reload_dhcp, + _apply_once('dhcp', tasks, firewall_queues, reload_dhcp, lambda: (dhcp(), )) - _apply_once('firewall', firewall_queues, reload_firewall, + _apply_once('firewall', tasks, firewall_queues, reload_firewall, lambda: (BuildFirewall().build_ipt())) - _apply_once('firewall_vlan', firewall_queues, reload_firewall_vlan, + _apply_once('firewall_vlan', tasks, firewall_queues, reload_firewall_vlan, lambda: (vlan(), )) - _apply_once('blacklist', firewall_queues, reload_blacklist, + _apply_once('blacklist', tasks, firewall_queues, reload_blacklist, lambda: (list(ipset()), )) diff --git a/circle/locale/hu/LC_MESSAGES/django.po b/circle/locale/hu/LC_MESSAGES/django.po index c50c2b4..b8c8759 100644 --- a/circle/locale/hu/LC_MESSAGES/django.po +++ b/circle/locale/hu/LC_MESSAGES/django.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-09-24 13:16+0200\n" -"PO-Revision-Date: 2014-09-24 13:18+0200\n" +"POT-Creation-Date: 2014-10-20 12:09+0200\n" +"PO-Revision-Date: 2014-10-20 13:01+0200\n" "Last-Translator: Mate Ory \n" "Language-Team: Hungarian \n" "Language: hu\n" @@ -25,90 +25,90 @@ msgstr "Angol" msgid "Hungarian" msgstr "Magyar" -#: common/models.py:62 +#: common/models.py:71 msgid "Failure." msgstr "Hiba." -#: common/models.py:63 +#: common/models.py:72 #, python-format msgid "Unhandled exception: %(error)s" msgstr "Kezeletlen kivétel: %(error)s" -#: common/models.py:138 +#: common/models.py:147 #: dashboard/templates/dashboard/instanceactivity_detail.html:28 msgid "activity code" msgstr "tevékenységkód" -#: common/models.py:141 +#: common/models.py:150 msgid "human readable name" msgstr "olvasható név" -#: common/models.py:142 +#: common/models.py:151 msgid "Human readable name of activity." msgstr "A tevékenység neve olvasható formában." -#: common/models.py:146 +#: common/models.py:155 msgid "Celery task unique identifier." msgstr "Celery feladat egyedi azonosítója." -#: common/models.py:147 +#: common/models.py:156 msgid "task_uuid" msgstr "feladat uuid" -#: common/models.py:148 +#: common/models.py:157 #: dashboard/templates/dashboard/instanceactivity_detail.html:37 -#: firewall/models.py:273 vm/models/common.py:84 vm/models/instance.py:140 -#: vm/models/instance.py:221 +#: firewall/models.py:273 vm/models/common.py:84 vm/models/instance.py:131 +#: vm/models/instance.py:212 msgid "user" msgstr "felhasználó" -#: common/models.py:149 +#: common/models.py:158 msgid "The person who started this activity." msgstr "A tevékenységet indító felhasználó." -#: common/models.py:150 +#: common/models.py:159 msgid "started at" msgstr "indítás ideje" -#: common/models.py:152 +#: common/models.py:161 msgid "Time of activity initiation." msgstr "A tevékenység megkezdésének időpontja." -#: common/models.py:153 +#: common/models.py:162 msgid "finished at" msgstr "befejezés ideje" -#: common/models.py:155 +#: common/models.py:164 msgid "Time of activity finalization." msgstr "A tevékenység befejeztének ideje." -#: common/models.py:157 +#: common/models.py:166 msgid "True, if the activity has finished successfully." msgstr "Igaz, ha a tevékenység sikeresen befejeződött." -#: common/models.py:159 +#: common/models.py:168 #: dashboard/templates/dashboard/instanceactivity_detail.html:56 msgid "result" msgstr "eredmény" -#: common/models.py:161 +#: common/models.py:170 msgid "Human readable result of activity." msgstr "A tevékenység eredménye olvasható formában." -#: common/models.py:523 +#: common/models.py:543 msgid "Permission Denied" msgstr "Hozzáférés megtagadva" -#: common/models.py:525 +#: common/models.py:545 msgid "Unknown error" msgstr "Ismeretlen hiba" -#: common/models.py:526 +#: common/models.py:546 #, python-format msgid "Unknown error: %(ex)s" msgstr "Ismeretlen hiba: %(ex)s" -#: common/operations.py:160 +#: common/operations.py:177 msgid "Superuser privileges are required." msgstr "Rendszergazdai jogosultság szükséges." @@ -121,24 +121,24 @@ msgstr "%s (csoport)" msgid "no matches found" msgstr "nincs találat" -#: dashboard/forms.py:65 +#: dashboard/forms.py:67 msgid "idle" msgstr "üresjáratban" -#: dashboard/forms.py:66 +#: dashboard/forms.py:68 msgid "normal" msgstr "normál" -#: dashboard/forms.py:67 +#: dashboard/forms.py:69 msgid "server" msgstr "szerver" -#: dashboard/forms.py:68 +#: dashboard/forms.py:70 msgid "realtime" msgstr "valós idejű" -#: dashboard/forms.py:73 dashboard/forms.py:776 dashboard/forms.py:797 -#: dashboard/forms.py:1084 dashboard/tables.py:225 +#: dashboard/forms.py:88 dashboard/forms.py:805 dashboard/forms.py:895 +#: dashboard/forms.py:1196 dashboard/tables.py:225 #: dashboard/templates/dashboard/_vm-create-2.html:20 #: dashboard/templates/dashboard/vm-list.html:60 #: dashboard/templates/dashboard/vm-detail/home.html:8 firewall/models.py:285 @@ -147,20 +147,31 @@ msgstr "valós idejű" msgid "Name" msgstr "Név" -#: dashboard/forms.py:74 vm/models/instance.py:145 +#: dashboard/forms.py:89 vm/models/instance.py:136 msgid "Human readable name of template." msgstr "A sablon olvasható neve." -#: dashboard/forms.py:190 dashboard/templates/dashboard/_vm-create-1.html:53 -#: dashboard/templates/dashboard/vm-detail/home.html:30 +#: dashboard/forms.py:99 +msgid "Clone template permissions" +msgstr "Sablon jogosultságainak klónozása" + +#: dashboard/forms.py:100 +msgid "" +"Clone the access list of parent template. Useful for updating a template." +msgstr "" +"A szülősablon hozzáférési listájának másolása. Sablonok frissítéséhez " +"ajánlott." + +#: dashboard/forms.py:211 dashboard/templates/dashboard/_vm-create-1.html:53 +#: dashboard/templates/dashboard/vm-detail/home.html:33 msgid "Description" msgstr "Leírás" -#: dashboard/forms.py:201 dashboard/forms.py:249 +#: dashboard/forms.py:222 dashboard/forms.py:269 msgid "Directory identifier" msgstr "Címtári azonosító" -#: dashboard/forms.py:204 +#: dashboard/forms.py:225 msgid "" "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 " @@ -171,7 +182,7 @@ msgstr "" "kerülnek, ha bejelentkeznek. Vegye figyelembe, hogy más, az önhöz hasonló " "jogosultságú felhasználók is csoportadminisztrátorrá válhatnak." -#: dashboard/forms.py:228 +#: dashboard/forms.py:249 #: dashboard/templates/dashboard/store/_list-box.html:57 #: network/templates/network/blacklist-create.html:8 #: network/templates/network/dashboard.html:25 @@ -193,13 +204,13 @@ msgstr "" msgid "Create" msgstr "Létrehozás" -#: dashboard/forms.py:257 dashboard/forms.py:1004 dashboard/forms.py:1021 -#: dashboard/forms.py:1047 dashboard/forms.py:1097 dashboard/forms.py:1138 -#: dashboard/forms.py:1158 dashboard/forms.py:1187 +#: dashboard/forms.py:277 dashboard/forms.py:1116 dashboard/forms.py:1133 +#: dashboard/forms.py:1159 dashboard/forms.py:1209 dashboard/forms.py:1250 +#: dashboard/forms.py:1270 dashboard/forms.py:1299 #: dashboard/templates/dashboard/_manage_access.html:73 #: dashboard/templates/dashboard/connect-command-create.html:37 #: dashboard/templates/dashboard/connect-command-edit.html:37 -#: dashboard/templates/dashboard/group-detail.html:102 +#: dashboard/templates/dashboard/group-detail.html:132 #: dashboard/templates/dashboard/lease-edit.html:96 #: dashboard/templates/dashboard/vm-detail/tx-owner.html:12 #: network/forms.py:82 network/forms.py:103 network/forms.py:139 @@ -208,175 +219,226 @@ msgstr "Létrehozás" msgid "Save" msgstr "Mentés" -#: dashboard/forms.py:289 dashboard/templates/dashboard/vm-detail.html:78 +#: dashboard/forms.py:307 dashboard/templates/dashboard/vm-detail.html:92 msgid "Host" msgstr "Gép" -#: dashboard/forms.py:359 dashboard/templates/dashboard/node-detail.html:4 -#: dashboard/templates/dashboard/vm-list.html:81 +#: dashboard/forms.py:378 dashboard/forms.py:776 dashboard/forms.py:933 +#: dashboard/templates/dashboard/node-detail.html:4 +#: dashboard/templates/dashboard/vm-list.html:85 +#: dashboard/templates/dashboard/vm-detail/home.html:116 msgid "Node" msgstr "Csomópont" -#: dashboard/forms.py:438 +#: dashboard/forms.py:457 msgid "Networks" msgstr "Hálózatok" -#: dashboard/forms.py:667 +#: dashboard/forms.py:683 msgid "Suspend in" msgstr "Felfüggesztés ideje" -#: dashboard/forms.py:671 dashboard/forms.py:695 +#: dashboard/forms.py:687 dashboard/forms.py:711 msgid "hours" msgstr "óra" -#: dashboard/forms.py:676 dashboard/forms.py:700 +#: dashboard/forms.py:692 dashboard/forms.py:716 msgid "days" msgstr "nap" -#: dashboard/forms.py:681 dashboard/forms.py:705 +#: dashboard/forms.py:697 dashboard/forms.py:721 msgid "weeks" msgstr "hét" -#: dashboard/forms.py:686 dashboard/forms.py:710 +#: dashboard/forms.py:702 dashboard/forms.py:726 msgid "months" msgstr "hónap" -#: dashboard/forms.py:691 +#: dashboard/forms.py:707 msgid "Delete in" msgstr "Törlés ideje" -#: dashboard/forms.py:716 dashboard/templates/dashboard/template-edit.html:64 +#: dashboard/forms.py:732 dashboard/templates/dashboard/template-edit.html:63 #: network/forms.py:60 msgid "Save changes" msgstr "Változások mentése" -#: dashboard/forms.py:726 +#: dashboard/forms.py:742 msgid "Set expiration times even if they are shorter than the current value." msgstr "" "Akkor is állítsa át a lejárati időket, ha rövidebbek lesznek a jelenleginél." -#: dashboard/forms.py:729 +#: dashboard/forms.py:745 msgid "Save selected lease." msgstr "Kiválasztott bérlet mentése." -#: dashboard/forms.py:738 +#: dashboard/forms.py:754 msgid "Length" msgstr "Hossz" -#: dashboard/forms.py:753 +#: dashboard/forms.py:762 +msgid "Live migration" +msgstr "Live migration" + +#: dashboard/forms.py:764 +msgid "" +"Live migration is a way of moving virtual machines between hosts with a " +"service interruption of at most some seconds. Please note that it can take " +"very long and cause much network traffic in case of busy machines." +msgstr "" +"A live migration lehetővé teszi virtuális gépek csomópontok közti mozgatását " +"legfeljebb néhány másodperces szolgáltatáskimaradással. Vegye figyelembe, " +"hogy ez terhelt gépek esetén sokáig tarthat és nagy hálózati forgalommal jár." + +#: dashboard/forms.py:782 msgid "Forcibly interrupt all running activities." msgstr "Futó tevékenységek erőltetett befejezése." -#: dashboard/forms.py:754 +#: dashboard/forms.py:783 msgid "Set all activities to finished state, but don't interrupt any tasks." msgstr "" "Minden tevékenység befejezettre állítása (a feladatok megszakítása nélkül)." -#: dashboard/forms.py:757 +#: dashboard/forms.py:786 msgid "New status" msgstr "Új állapot" -#: dashboard/forms.py:778 +#: dashboard/forms.py:787 +msgid "Reset node" +msgstr "Csomópont visszaállítása" + +#: dashboard/forms.py:801 +msgid "use emergency state change" +msgstr "vész-állapotváltás használata" + +#: dashboard/forms.py:807 dashboard/forms.py:827 #: dashboard/templates/dashboard/store/_list-box.html:117 msgid "Size" msgstr "Méret" -#: dashboard/forms.py:779 +#: dashboard/forms.py:808 msgid "Size of disk to create in bytes or with units like MB or GB." msgstr "Létrehozandó lemez mérete byte-okban vagy mértékegységgel (MB, GB)." -#: dashboard/forms.py:785 +#: dashboard/forms.py:820 dashboard/forms.py:849 msgid "Invalid format, you can use GB or MB!" msgstr "Érvénytelen formátum. „GB” és „MB” is használható." -#: dashboard/forms.py:798 +#: dashboard/forms.py:828 +msgid "Size to resize the disk in bytes or with units like MB or GB." +msgstr "A lemez kívánt mérete byte-okban vagy mértékegységgel (MB, GB)." + +#: dashboard/forms.py:839 dashboard/forms.py:875 +msgid "Disk" +msgstr "Lemez" + +#: dashboard/forms.py:852 +msgid "Disk size must be greater than the actual size." +msgstr "A lemez mérete nagyobb kell legyen a jelenleginél." + +#: dashboard/forms.py:861 dashboard/forms.py:886 +#, python-format +msgid " %s" +msgstr " %s" + +#: dashboard/forms.py:896 msgid "URL" msgstr "URL" -#: dashboard/forms.py:813 +#: dashboard/forms.py:906 +msgid "Could not find filename in URL, please specify a name explicitly." +msgstr "Az URL-ben nem található fájlnév. Kérem adja meg explicite." + +#: dashboard/forms.py:917 #: dashboard/templates/dashboard/node-detail/resources.html:17 msgid "Vlan" msgstr "Vlan" -#: dashboard/forms.py:816 +#: dashboard/forms.py:920 msgid "No more networks." msgstr "Nincs több hálózat." -#: dashboard/forms.py:844 dashboard/templates/dashboard/profile.html:31 -#: dashboard/templates/dashboard/vm-detail.html:94 +#: dashboard/forms.py:934 +msgid "" +"Deploy virtual machine to this node (blank allows scheduling automatically)." +msgstr "" +"A virtuális gép elindítása ezen a csomóponton (üresen hagyva automatikus " +"ütemezés)." + +#: dashboard/forms.py:956 dashboard/templates/dashboard/profile.html:31 +#: dashboard/templates/dashboard/vm-detail.html:108 msgid "Username" msgstr "Felhasználónév" -#: dashboard/forms.py:858 dashboard/templates/dashboard/vm-detail.html:96 +#: dashboard/forms.py:970 dashboard/templates/dashboard/vm-detail.html:110 msgid "Password" msgstr "Jelszó" -#: dashboard/forms.py:863 +#: dashboard/forms.py:975 msgid "Sign in" msgstr "Bejelentkezés" -#: dashboard/forms.py:886 dashboard/templates/dashboard/profile.html:37 +#: dashboard/forms.py:998 dashboard/templates/dashboard/profile.html:37 msgid "Email address" msgstr "E-mail cím" -#: dashboard/forms.py:891 +#: dashboard/forms.py:1003 msgid "Reset password" msgstr "Új jelszó" -#: dashboard/forms.py:907 dashboard/forms.py:1030 +#: dashboard/forms.py:1019 dashboard/forms.py:1142 msgid "Change password" msgstr "Jelszóváltoztatás" -#: dashboard/forms.py:979 +#: dashboard/forms.py:1091 msgid "Add trait" msgstr "Jellemző hozzáadása" -#: dashboard/forms.py:1061 dashboard/templates/dashboard/lease-edit.html:86 +#: dashboard/forms.py:1173 dashboard/templates/dashboard/lease-edit.html:86 msgid "Name of group or user" msgstr "Csoport vagy felhasználó neve" -#: dashboard/forms.py:1069 dashboard/forms.py:1078 +#: dashboard/forms.py:1181 dashboard/forms.py:1190 msgid "Name of user" msgstr "Felhasználó neve" -#: dashboard/forms.py:1071 dashboard/forms.py:1080 +#: dashboard/forms.py:1183 dashboard/forms.py:1192 msgid "E-mail address or identifier of user" msgstr "A felhasználó e-mail címe vagy azonosítója" -#: dashboard/forms.py:1086 +#: dashboard/forms.py:1198 msgid "Key" msgstr "Kulcs" -#: dashboard/forms.py:1087 +#: dashboard/forms.py:1199 msgid "For example: ssh-rsa AAAAB3NzaC1yc2ED..." msgstr "Például: ssh-rsa AAAAB3NzaC1yc2ED…" -#: dashboard/forms.py:1173 +#: dashboard/forms.py:1285 msgid "permissions" msgstr "jogosultságok" -#: dashboard/forms.py:1230 +#: dashboard/forms.py:1342 msgid "owned" msgstr "saját" -#: dashboard/forms.py:1231 +#: dashboard/forms.py:1343 msgid "shared" msgstr "osztott" -#: dashboard/forms.py:1232 +#: dashboard/forms.py:1344 msgid "all" msgstr "összes" -#: dashboard/forms.py:1239 dashboard/forms.py:1263 +#: dashboard/forms.py:1351 dashboard/forms.py:1375 #: dashboard/templates/dashboard/index-groups.html:21 -#: dashboard/templates/dashboard/index-nodes.html:34 +#: dashboard/templates/dashboard/index-nodes.html:61 #: dashboard/templates/dashboard/index-vm.html:57 msgid "Search..." msgstr "Keresés..." #: dashboard/models.py:65 dashboard/templates/dashboard/index-groups.html:39 -#: dashboard/templates/dashboard/index-nodes.html:48 -#: dashboard/templates/dashboard/index-nodes.html:73 +#: dashboard/templates/dashboard/index-nodes.html:78 #: dashboard/templates/dashboard/index-templates.html:38 #: dashboard/templates/dashboard/index-vm.html:76 #: dashboard/templates/dashboard/store/_list-box.html:101 @@ -391,7 +453,7 @@ msgstr "kézbesített" msgid "read" msgstr "olvasott" -#: dashboard/models.py:108 vm/models/instance.py:107 +#: dashboard/models.py:108 vm/models/instance.py:98 msgid "access method" msgstr "elérés módja" @@ -402,8 +464,8 @@ msgstr "Távoli elérési mód típusa." #: dashboard/models.py:110 firewall/models.py:413 firewall/models.py:440 #: firewall/models.py:828 firewall/models.py:850 firewall/models.py:871 #: storage/models.py:47 storage/models.py:86 vm/models/common.py:65 -#: vm/models/common.py:89 vm/models/common.py:165 vm/models/instance.py:144 -#: vm/models/instance.py:234 vm/models/node.py:65 +#: vm/models/common.py:89 vm/models/common.py:165 vm/models/instance.py:135 +#: vm/models/instance.py:225 vm/models/node.py:65 msgid "name" msgstr "név" @@ -468,14 +530,14 @@ msgid "Can use autocomplete." msgstr "Használhat automatikus kiegészítést." #: dashboard/models.py:229 firewall/models.py:274 vm/models/common.py:85 -#: vm/models/instance.py:141 vm/models/instance.py:222 +#: vm/models/instance.py:132 vm/models/instance.py:213 msgid "operator" msgstr "operátor" #: dashboard/models.py:230 firewall/models.py:100 firewall/models.py:369 #: firewall/models.py:422 firewall/models.py:445 firewall/models.py:510 #: firewall/models.py:851 firewall/models.py:880 vm/models/common.py:86 -#: vm/models/instance.py:142 vm/models/instance.py:223 +#: vm/models/instance.py:133 vm/models/instance.py:214 msgid "owner" msgstr "tulajdonos" @@ -501,12 +563,12 @@ msgstr "Állapot" #: dashboard/tables.py:145 dashboard/templates/dashboard/_vm-create-2.html:38 #: dashboard/templates/dashboard/node-detail.html:69 -#: dashboard/templates/dashboard/vm-detail.html:163 +#: dashboard/templates/dashboard/vm-detail.html:177 msgid "Resources" msgstr "Erőforrások" #: dashboard/tables.py:151 dashboard/templates/dashboard/vm-list.html:72 -#: vm/models/instance.py:113 +#: vm/models/instance.py:104 msgid "Lease" msgstr "Bérlet" @@ -545,7 +607,7 @@ msgid "Access method" msgstr "Elérés módja" #: dashboard/tables.py:265 -#: dashboard/templates/dashboard/vm-detail/home.html:94 +#: dashboard/templates/dashboard/vm-detail/home.html:129 msgid "Template" msgstr "Sablon" @@ -659,33 +721,37 @@ msgstr "" msgid "I have the Client installed" msgstr "Már telepítve van" -#: dashboard/templates/dashboard/_disk-list-element.html:10 -#: dashboard/templates/dashboard/node-detail/_activity-timeline.html:28 -#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:45 -msgid "failed" -msgstr "meghiúsult" - -#: dashboard/templates/dashboard/_disk-list-element.html:16 -#: dashboard/templates/dashboard/_disk-list-element.html:18 +#: dashboard/templates/dashboard/_disk-list-element.html:12 #: dashboard/templates/dashboard/_manage_access.html:34 #: dashboard/templates/dashboard/_manage_access.html:56 -#: dashboard/templates/dashboard/group-detail.html:63 +#: dashboard/templates/dashboard/group-detail.html:93 #: dashboard/templates/dashboard/lease-edit.html:60 #: dashboard/templates/dashboard/lease-edit.html:80 +#: dashboard/templates/dashboard/template-edit.html:107 +#: dashboard/templates/dashboard/template-edit.html:108 #: dashboard/templates/dashboard/confirm/base-remove.html:12 #: dashboard/templates/dashboard/store/_list-box.html:133 #: dashboard/templates/dashboard/store/remove.html:36 -#: dashboard/templates/dashboard/vm-detail/network.html:79 -#: dashboard/templates/dashboard/vm-detail/network.html:111 +#: dashboard/templates/dashboard/vm-detail/network.html:81 +#: dashboard/templates/dashboard/vm-detail/network.html:113 msgid "Remove" msgstr "Eltávolítás" +#: dashboard/templates/dashboard/_disk-list-element.html:21 +msgid "Resize" +msgstr "Átméretezés" + +#: dashboard/templates/dashboard/_disk-list-element.html:28 +#: dashboard/templates/dashboard/store/remove.html:20 +msgid "File name" +msgstr "Fájlnév" + #: dashboard/templates/dashboard/_display-name.html:10 msgid "username" msgstr "felhasználónév" #: dashboard/templates/dashboard/_manage_access.html:7 -#: dashboard/templates/dashboard/group-detail.html:63 +#: dashboard/templates/dashboard/group-detail.html:93 #: dashboard/templates/dashboard/lease-edit.html:35 msgid "Who" msgstr "Ki" @@ -746,17 +812,17 @@ msgid "Next" msgstr "Tovább" #: dashboard/templates/dashboard/_template-create.html:15 -#: dashboard/templates/dashboard/template-edit.html:40 +#: dashboard/templates/dashboard/template-edit.html:39 msgid "Resource configuration" msgstr "Erőforrásbeállítások" #: dashboard/templates/dashboard/_template-create.html:21 -#: dashboard/templates/dashboard/template-edit.html:46 +#: dashboard/templates/dashboard/template-edit.html:45 msgid "Virtual machine settings" msgstr "Virtuális gépek beállításai" #: dashboard/templates/dashboard/_template-create.html:31 -#: dashboard/templates/dashboard/template-edit.html:57 +#: dashboard/templates/dashboard/template-edit.html:56 msgid "External resources" msgstr "Külső erőforrások" @@ -780,20 +846,21 @@ msgid "CPU" msgstr "CPU" #: dashboard/templates/dashboard/_vm-create-1.html:23 +#: dashboard/templates/dashboard/vm-list.html:76 #: dashboard/templates/dashboard/node-list/column-monitor.html:20 msgid "Memory" msgstr "Memória" #: dashboard/templates/dashboard/_vm-create-1.html:33 #: dashboard/templates/dashboard/_vm-create-2.html:49 -#: dashboard/templates/dashboard/vm-detail/resources.html:28 +#: dashboard/templates/dashboard/vm-detail/resources.html:25 msgid "Disks" msgstr "Lemezek" #: dashboard/templates/dashboard/_vm-create-1.html:40 #: dashboard/templates/dashboard/_vm-create-2.html:65 #: dashboard/templates/dashboard/base.html:46 -#: dashboard/templates/dashboard/vm-detail.html:177 +#: dashboard/templates/dashboard/vm-detail.html:191 #: dashboard/views/graph.py:192 dashboard/views/graph.py:215 #: network/forms.py:123 network/templates/network/base.html:7 msgid "Network" @@ -826,6 +893,7 @@ msgid "Amount" msgstr "Mennyiség" #: dashboard/templates/dashboard/_vm-create-2.html:56 +#: dashboard/templates/dashboard/vm-detail/resources.html:34 msgid "No disks are added." msgstr "Egy lemez sincs hozzáadva." @@ -833,25 +901,25 @@ msgstr "Egy lemez sincs hozzáadva." msgid "Not added to any network." msgstr "Egy hálózathoz sincs hozzáadva." -#: dashboard/templates/dashboard/_vm-mass-migrate.html:12 +#: dashboard/templates/dashboard/_vm-mass-migrate.html:13 msgid "Reschedule" msgstr "Újraütemezés" -#: dashboard/templates/dashboard/_vm-mass-migrate.html:16 +#: dashboard/templates/dashboard/_vm-mass-migrate.html:17 msgid "This option will reschedule each virtual machine to the optimal node." msgstr "Ez a lehetőség minden virtuális gépet az optimális csomópontra migrál." -#: dashboard/templates/dashboard/_vm-mass-migrate.html:28 -#: dashboard/templates/dashboard/_vm-migrate.html:27 +#: dashboard/templates/dashboard/_vm-mass-migrate.html:29 +#: dashboard/templates/dashboard/_vm-migrate.html:31 msgid "CPU load" msgstr "CPU-terhelés" -#: dashboard/templates/dashboard/_vm-mass-migrate.html:29 -#: dashboard/templates/dashboard/_vm-migrate.html:28 +#: dashboard/templates/dashboard/_vm-mass-migrate.html:30 +#: dashboard/templates/dashboard/_vm-migrate.html:33 msgid "RAM usage" msgstr "RAM-használat" -#: dashboard/templates/dashboard/_vm-migrate.html:7 +#: dashboard/templates/dashboard/_vm-migrate.html:8 #, python-format msgid "" "\n" @@ -860,11 +928,11 @@ msgstr "" "\n" "Válasszon csomópontot %(obj)s migrálásához.\n" -#: dashboard/templates/dashboard/_vm-migrate.html:21 +#: dashboard/templates/dashboard/_vm-migrate.html:24 msgid "current" msgstr "jelenlegi" -#: dashboard/templates/dashboard/_vm-migrate.html:22 +#: dashboard/templates/dashboard/_vm-migrate.html:25 msgid "recommended" msgstr "javasolt" @@ -914,7 +982,7 @@ msgstr "Parancssablon létrehozása" #: dashboard/templates/dashboard/lease-create.html:13 #: dashboard/templates/dashboard/lease-edit.html:12 #: dashboard/templates/dashboard/profile.html:19 -#: dashboard/templates/dashboard/template-edit.html:14 +#: dashboard/templates/dashboard/template-edit.html:15 #: dashboard/templates/dashboard/userkey-create.html:13 #: dashboard/templates/dashboard/userkey-edit.html:14 #: dashboard/templates/dashboard/confirm/base-renew.html:26 @@ -956,10 +1024,10 @@ msgstr "csoport" #: dashboard/templates/dashboard/group-detail.html:33 #: dashboard/templates/dashboard/node-detail.html:13 #: dashboard/templates/dashboard/node-detail.html:21 -#: dashboard/templates/dashboard/vm-detail.html:50 +#: dashboard/templates/dashboard/vm-detail.html:63 #: dashboard/templates/dashboard/group-list/column-name.html:7 #: dashboard/templates/dashboard/node-list/column-name.html:7 -#: dashboard/templates/dashboard/vm-detail/home.html:22 +#: dashboard/templates/dashboard/vm-detail/home.html:24 #: dashboard/templates/dashboard/vm-list/column-name.html:7 msgid "Rename" msgstr "Átnevezés" @@ -967,6 +1035,7 @@ msgstr "Átnevezés" #: dashboard/templates/dashboard/group-detail.html:12 #: dashboard/templates/dashboard/group-detail.html:37 #: dashboard/templates/dashboard/node-detail.html:14 +#: dashboard/templates/dashboard/template-edit.html:75 #: dashboard/templates/dashboard/confirm/ajax-delete.html:18 #: dashboard/templates/dashboard/confirm/mass-delete.html:12 #: dashboard/templates/dashboard/connect-command-list/column-command-actions.html:5 @@ -985,28 +1054,32 @@ msgid "Delete group." msgstr "Csoport törlése." #: dashboard/templates/dashboard/group-detail.html:55 +msgid "Available objects for this group" +msgstr "Csoport számára elérhető objektumok" + +#: dashboard/templates/dashboard/group-detail.html:85 msgid "User list" msgstr "Felhasználók" -#: dashboard/templates/dashboard/group-detail.html:57 +#: dashboard/templates/dashboard/group-detail.html:87 msgid "Create user" msgstr "Új felhasználó" -#: dashboard/templates/dashboard/group-detail.html:74 -#: dashboard/templates/dashboard/group-detail.html:87 -#: dashboard/templates/dashboard/vm-detail/network.html:27 +#: dashboard/templates/dashboard/group-detail.html:104 +#: dashboard/templates/dashboard/group-detail.html:117 +#: dashboard/templates/dashboard/vm-detail/network.html:28 msgid "remove" msgstr "eltávolítás" -#: dashboard/templates/dashboard/group-detail.html:100 +#: dashboard/templates/dashboard/group-detail.html:130 msgid "Add multiple users at once (one identifier per line)." msgstr "Több felhasználó hozzáadása (egy azonosító soronként)." -#: dashboard/templates/dashboard/group-detail.html:107 +#: dashboard/templates/dashboard/group-detail.html:137 msgid "Access permissions" msgstr "Hozzáférési jogosultságok" -#: dashboard/templates/dashboard/group-detail.html:116 +#: dashboard/templates/dashboard/group-detail.html:146 msgid "Group permissions" msgstr "Csoportjogosultságok" @@ -1024,7 +1097,7 @@ msgstr "Azon csoportok, amelyekhez hozzáférése van." #: dashboard/templates/dashboard/index-groups.html:7 #: dashboard/templates/dashboard/profile.html:56 -#: dashboard/templates/dashboard/vm-detail/network.html:37 +#: dashboard/templates/dashboard/vm-detail/network.html:39 #: network/templates/network/host-edit.html:32 templates/info/help.html:192 msgid "Groups" msgstr "Csoportok" @@ -1049,12 +1122,12 @@ msgstr[1] "" " " #: dashboard/templates/dashboard/index-groups.html:36 -#: dashboard/templates/dashboard/index-nodes.html:45 +#: dashboard/templates/dashboard/index-nodes.html:74 #: dashboard/templates/dashboard/index-vm.html:73 msgid "list" msgstr "felsorolás" -#: dashboard/templates/dashboard/index-nodes.html:12 +#: dashboard/templates/dashboard/index-nodes.html:11 msgid "" "List of compute nodes, also called worker nodes or hypervisors, which run " "the virtual machines." @@ -1062,13 +1135,16 @@ msgstr "" "A virtuális gépeket futtató számítási csomópontok (más néven worker node-ok, " "hypervisorok) listája." -#: dashboard/templates/dashboard/index-nodes.html:15 +#: dashboard/templates/dashboard/index-nodes.html:16 #: dashboard/templates/dashboard/node-list.html:5 msgid "Nodes" msgstr "Csomópontok" -#: dashboard/templates/dashboard/index-nodes.html:43 -#: dashboard/templates/dashboard/index-nodes.html:70 +#: dashboard/templates/dashboard/index-nodes.html:63 +msgid "Search" +msgstr "Keresés" + +#: dashboard/templates/dashboard/index-nodes.html:72 #, python-format msgid "%(count)s more" msgstr "még %(count)s" @@ -1132,7 +1208,7 @@ msgid "Mark as favorite" msgstr "Kedvencnek jelölés" #: dashboard/templates/dashboard/index-vm.html:43 -#: dashboard/templates/dashboard/vm-list.html:124 +#: dashboard/templates/dashboard/vm-list.html:135 msgid "You have no virtual machines." msgstr "Még nincs virtuális gépe." @@ -1195,14 +1271,14 @@ msgstr "Nincs jogosultsága virtuális gépek indítására vagy kezelésére." #: dashboard/templates/dashboard/instanceactivity_detail.html:25 #: dashboard/templates/dashboard/node-detail.html:82 -#: dashboard/templates/dashboard/vm-detail.html:182 +#: dashboard/templates/dashboard/vm-detail.html:196 #: dashboard/templates/dashboard/node-detail/activity.html:3 #: dashboard/templates/dashboard/vm-detail/activity.html:3 msgid "Activity" msgstr "Tevékenységek" #: dashboard/templates/dashboard/instanceactivity_detail.html:31 -#: vm/models/activity.py:71 vm/models/instance.py:282 vm/models/network.py:68 +#: vm/models/activity.py:70 vm/models/instance.py:274 vm/models/network.py:67 msgid "instance" msgstr "példány" @@ -1242,6 +1318,20 @@ msgstr "állapot" msgid "resultant state" msgstr "új állapot" +#: dashboard/templates/dashboard/instanceactivity_detail.html:62 +msgid "subactivities" +msgstr "altevékenységek" + +#: dashboard/templates/dashboard/instanceactivity_detail.html:74 +#: dashboard/templates/dashboard/node-detail/_activity-timeline.html:28 +#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:48 +msgid "failed" +msgstr "meghiúsult" + +#: dashboard/templates/dashboard/instanceactivity_detail.html:78 +msgid "none" +msgstr "nincs" + #: dashboard/templates/dashboard/lease-create.html:5 #: dashboard/templates/dashboard/lease-create.html:14 msgid "Create lease" @@ -1253,7 +1343,7 @@ msgid "Edit lease" msgstr "Bérlési mód szerkesztése" #: dashboard/templates/dashboard/lease-edit.html:27 -#: dashboard/templates/dashboard/template-edit.html:73 +#: dashboard/templates/dashboard/template-edit.html:84 #: network/templates/network/vlan-edit.html:26 msgid "Manage access" msgstr "Jogosultságok kezelése" @@ -1307,7 +1397,7 @@ msgid "Offline" msgstr "Offline" #: dashboard/templates/dashboard/node-detail.html:63 -#: dashboard/templates/dashboard/vm-detail.html:158 +#: dashboard/templates/dashboard/vm-detail.html:172 msgid "Home" msgstr "Kezdőoldal" @@ -1431,24 +1521,27 @@ msgid "Command templates" msgstr "Parancssablonok" #: dashboard/templates/dashboard/template-edit.html:6 -#: vm/models/instance.py:167 vm/models/instance.py:240 vm/models/network.py:45 +#: vm/models/instance.py:158 vm/models/instance.py:231 vm/models/network.py:44 msgid "template" msgstr "sablon" -#: dashboard/templates/dashboard/template-edit.html:15 +#: dashboard/templates/dashboard/template-edit.html:17 msgid "Edit template" msgstr "Sablon szerkesztése" -#: dashboard/templates/dashboard/template-edit.html:32 -msgid "Visit" -msgstr "Megtekintés" +#: dashboard/templates/dashboard/template-edit.html:29 +msgid "Parent template" +msgstr "Szülősablon" -#: dashboard/templates/dashboard/template-edit.html:83 +#: dashboard/templates/dashboard/template-edit.html:77 +msgid "Delete template" +msgstr "Sablon törlése" + +#: dashboard/templates/dashboard/template-edit.html:94 msgid "Disk list" msgstr "Lemezek" -#: dashboard/templates/dashboard/template-edit.html:88 -#: dashboard/templates/dashboard/vm-detail/resources.html:39 +#: dashboard/templates/dashboard/template-edit.html:99 msgid "No disks are added!" msgstr "Egy lemez sincs hozzáadva!" @@ -1478,131 +1571,135 @@ msgstr "SSH publikus kulcs létrehozása" msgid "Edit SSH public key" msgstr "SSH publikus kulcs módosítása" -#: dashboard/templates/dashboard/vm-detail.html:10 -msgid "This is the master vm of your new template" -msgstr "Ez a mesterpéldány egy új sablonhoz" +#: dashboard/templates/dashboard/vm-detail.html:18 +msgid "Toggle tutorial panel" +msgstr "Kalauz engedélyezése/tiltása" -#: dashboard/templates/dashboard/vm-detail.html:13 +#: dashboard/templates/dashboard/vm-detail.html:23 msgid "Start template tutorial" msgstr "Sablon-kalauz indítása" -#: dashboard/templates/dashboard/vm-detail.html:17 +#: dashboard/templates/dashboard/vm-detail.html:26 +msgid "This is the master vm of your new template" +msgstr "Ez a mesterpéldány egy új sablonhoz" + +#: dashboard/templates/dashboard/vm-detail.html:28 msgid "" "Modify the virtual machine to suit your needs (optional)" msgstr "Módosítsa a virtuális gépet (igény szerint)" -#: dashboard/templates/dashboard/vm-detail.html:19 +#: dashboard/templates/dashboard/vm-detail.html:30 msgid "Change the description" msgstr "Változtassa meg a leírást" -#: dashboard/templates/dashboard/vm-detail.html:20 +#: dashboard/templates/dashboard/vm-detail.html:31 msgid "Change resources (CPU and RAM)" msgstr "Állítsa be az erőforrásokat (CPU és memória)" -#: dashboard/templates/dashboard/vm-detail.html:21 +#: dashboard/templates/dashboard/vm-detail.html:32 msgid "Attach or detach disks" msgstr "Csatoljon vagy válasszon le lemezeket" -#: dashboard/templates/dashboard/vm-detail.html:22 +#: dashboard/templates/dashboard/vm-detail.html:33 msgid "Add or remove network interfaces" msgstr "Adjon hozz vagy törljön hálózati interfészeket" -#: dashboard/templates/dashboard/vm-detail.html:25 +#: dashboard/templates/dashboard/vm-detail.html:36 msgid "Deploy the virtual machine" msgstr "Indítsa el a virtuális gépet" -#: dashboard/templates/dashboard/vm-detail.html:26 +#: dashboard/templates/dashboard/vm-detail.html:37 msgid "Connect to the machine" msgstr "Csatlakozzon a géphez" -#: dashboard/templates/dashboard/vm-detail.html:27 +#: dashboard/templates/dashboard/vm-detail.html:38 msgid "Do all the needed installations/customizations" msgstr "Végezze el a szükséges telepítéseket, testreszabásokat" -#: dashboard/templates/dashboard/vm-detail.html:28 +#: dashboard/templates/dashboard/vm-detail.html:39 msgid "Log off from the machine" msgstr "Jelentkezzen ki a gépből" -#: dashboard/templates/dashboard/vm-detail.html:30 +#: dashboard/templates/dashboard/vm-detail.html:41 msgid "Press the Save as template button" msgstr "Kattintson a Mentés sablonként gombra" -#: dashboard/templates/dashboard/vm-detail.html:33 +#: dashboard/templates/dashboard/vm-detail.html:44 msgid "Delete this virtual machine (optional)" msgstr "Törölje a virtális gépet (ha szükséges)" -#: dashboard/templates/dashboard/vm-detail.html:74 +#: dashboard/templates/dashboard/vm-detail.html:88 msgid "Connection details" msgstr "Kapcsolat részletei" -#: dashboard/templates/dashboard/vm-detail.html:76 +#: dashboard/templates/dashboard/vm-detail.html:90 msgid "Protocol" msgstr "Protokoll" -#: dashboard/templates/dashboard/vm-detail.html:83 +#: dashboard/templates/dashboard/vm-detail.html:97 msgid "The VM doesn't have any network interface." msgstr "A VM-nek nincs hálózati interfésze." -#: dashboard/templates/dashboard/vm-detail.html:85 +#: dashboard/templates/dashboard/vm-detail.html:99 msgid "The required port for this protocol is not forwarded." msgstr "A csatlakozáshoz szükséges port nincs továbbítva." -#: dashboard/templates/dashboard/vm-detail.html:90 +#: dashboard/templates/dashboard/vm-detail.html:104 msgid "Host (IPv6)" msgstr "Gép (IPv6)" -#: dashboard/templates/dashboard/vm-detail.html:102 +#: dashboard/templates/dashboard/vm-detail.html:116 msgid "Show password" msgstr "Jelszó megjelenítése" -#: dashboard/templates/dashboard/vm-detail.html:110 +#: dashboard/templates/dashboard/vm-detail.html:124 msgid "Start the VM to change the password." msgstr "Jelszóváltoztatáshoz el kell indítani a gépet." -#: dashboard/templates/dashboard/vm-detail.html:110 +#: dashboard/templates/dashboard/vm-detail.html:124 msgid "Generate new password!" msgstr "Új jelszó generálása" -#: dashboard/templates/dashboard/vm-detail.html:117 -#: dashboard/templates/dashboard/vm-detail.html:129 +#: dashboard/templates/dashboard/vm-detail.html:131 +#: dashboard/templates/dashboard/vm-detail.html:143 msgid "Command" msgstr "Parancs" -#: dashboard/templates/dashboard/vm-detail.html:122 -#: dashboard/templates/dashboard/vm-detail.html:133 +#: dashboard/templates/dashboard/vm-detail.html:136 +#: dashboard/templates/dashboard/vm-detail.html:147 #: dashboard/templates/dashboard/vm-list.html:22 msgid "Select all" msgstr "Összes kiválasztása" -#: dashboard/templates/dashboard/vm-detail.html:130 +#: dashboard/templates/dashboard/vm-detail.html:144 msgid "Connection is not possible." msgstr "A csatlakozás nem lehetséges." -#: dashboard/templates/dashboard/vm-detail.html:140 +#: dashboard/templates/dashboard/vm-detail.html:154 msgid "Connect via the CIRCLE Client" msgstr "Csatlakozás CIRCLE klienssel" -#: dashboard/templates/dashboard/vm-detail.html:141 +#: dashboard/templates/dashboard/vm-detail.html:155 msgid "Connect" msgstr "Csatlakozás" -#: dashboard/templates/dashboard/vm-detail.html:143 +#: dashboard/templates/dashboard/vm-detail.html:157 msgid "Download client" msgstr "Kliens letöltése" -#: dashboard/templates/dashboard/vm-detail.html:145 +#: dashboard/templates/dashboard/vm-detail.html:159 msgid "Download the CIRCLE Client" msgstr "A CIRCLE kliens letöltése" -#: dashboard/templates/dashboard/vm-detail.html:146 +#: dashboard/templates/dashboard/vm-detail.html:160 msgid "Connect (download client)" msgstr "Csatlakozás (kliens letöltése)" -#: dashboard/templates/dashboard/vm-detail.html:168 +#: dashboard/templates/dashboard/vm-detail.html:182 msgid "Console" msgstr "Konzol" -#: dashboard/templates/dashboard/vm-detail.html:172 +#: dashboard/templates/dashboard/vm-detail.html:186 msgid "Access" msgstr "Hozzáférés" @@ -1626,15 +1723,15 @@ msgstr "ID" msgid "State" msgstr "Állapot" -#: dashboard/templates/dashboard/vm-list.html:77 +#: dashboard/templates/dashboard/vm-list.html:81 msgid "IP address" msgstr "IP cím" -#: dashboard/templates/dashboard/vm-list.html:122 +#: dashboard/templates/dashboard/vm-list.html:133 msgid "No result." msgstr "Nincs eredmény." -#: dashboard/templates/dashboard/vm-list.html:141 +#: dashboard/templates/dashboard/vm-list.html:152 msgid "" "You can select multiple vm instances while holding down the CTRL key." @@ -1642,7 +1739,7 @@ msgstr "" "Több virtuális gépet is kiválaszthat a CTRL billentyű " "lenyomásával." -#: dashboard/templates/dashboard/vm-list.html:142 +#: dashboard/templates/dashboard/vm-list.html:153 msgid "" "If you want to select multiple instances by one click select an instance " "then hold down SHIFT key and select another one!" @@ -1990,7 +2087,7 @@ msgstr "Felsorolás" #: dashboard/templates/dashboard/store/list.html:4 #: dashboard/templates/dashboard/store/upload.html:4 -#: dashboard/templates/dashboard/vm-detail/home.html:111 +#: dashboard/templates/dashboard/vm-detail/home.html:146 msgid "Store" msgstr "Tárhely" @@ -2025,10 +2122,6 @@ msgstr "Fájl törlésének megerősítése" msgid "File directory" msgstr "Fájl helye" -#: dashboard/templates/dashboard/store/remove.html:20 -msgid "File name" -msgstr "Fájlnév" - #: dashboard/templates/dashboard/store/remove.html:22 #, python-format msgid "" @@ -2077,19 +2170,19 @@ msgstr[1] "" "\n" " %(num_cores)s CPU mag\n" -#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:29 +#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:31 msgid "Abort" msgstr "Megszakítás" -#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:59 +#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:62 msgid "Show less activities" msgstr "Kevesebb tevékenység megjelenítése" -#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:61 +#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:64 msgid "Show all activities" msgstr "Összes tevékenység megjelenítése" -#: dashboard/templates/dashboard/vm-detail/_network-port-add.html:14 +#: dashboard/templates/dashboard/vm-detail/_network-port-add.html:15 msgid "Add" msgstr "Hozzáadás" @@ -2137,35 +2230,35 @@ msgstr "Bezárás" msgid "System" msgstr "Rendszer" -#: dashboard/templates/dashboard/vm-detail/home.html:42 +#: dashboard/templates/dashboard/vm-detail/home.html:48 msgid "Update" msgstr "Frissítés" -#: dashboard/templates/dashboard/vm-detail/home.html:49 +#: dashboard/templates/dashboard/vm-detail/home.html:57 msgid "Expiration" msgstr "Lejárat" -#: dashboard/templates/dashboard/vm-detail/home.html:60 +#: dashboard/templates/dashboard/vm-detail/home.html:69 msgid "Suspended at:" msgstr "Felfüggesztve:" -#: dashboard/templates/dashboard/vm-detail/home.html:62 +#: dashboard/templates/dashboard/vm-detail/home.html:75 msgid "Destroyed at:" msgstr "Megsemmisítve:" -#: dashboard/templates/dashboard/vm-detail/home.html:66 +#: dashboard/templates/dashboard/vm-detail/home.html:84 msgid "Tags" msgstr "Címkék" -#: dashboard/templates/dashboard/vm-detail/home.html:77 -msgid "No tag added!" +#: dashboard/templates/dashboard/vm-detail/home.html:97 +msgid "No tag added." msgstr "Nincs címke." -#: dashboard/templates/dashboard/vm-detail/home.html:88 +#: dashboard/templates/dashboard/vm-detail/home.html:109 msgid "Add tag" msgstr "Címke hozzáadása" -#: dashboard/templates/dashboard/vm-detail/network.html:8 vm/operations.py:123 +#: dashboard/templates/dashboard/vm-detail/network.html:8 vm/operations.py:201 msgid "add interface" msgstr "új interfész" @@ -2181,35 +2274,35 @@ msgstr "nem menedzselt" msgid "edit" msgstr "szerkesztés" -#: dashboard/templates/dashboard/vm-detail/network.html:34 +#: dashboard/templates/dashboard/vm-detail/network.html:36 #: firewall/models.py:482 msgid "IPv4 address" msgstr "IPv4 cím" -#: dashboard/templates/dashboard/vm-detail/network.html:35 +#: dashboard/templates/dashboard/vm-detail/network.html:37 #: firewall/models.py:492 msgid "IPv6 address" msgstr "IPv6 cím" -#: dashboard/templates/dashboard/vm-detail/network.html:36 +#: dashboard/templates/dashboard/vm-detail/network.html:38 msgid "DNS name" msgstr "DNS név" -#: dashboard/templates/dashboard/vm-detail/network.html:49 +#: dashboard/templates/dashboard/vm-detail/network.html:51 #: network/forms.py:246 msgid "IPv4" msgstr "IPv4" -#: dashboard/templates/dashboard/vm-detail/network.html:50 +#: dashboard/templates/dashboard/vm-detail/network.html:52 #: network/forms.py:253 msgid "IPv6" msgstr "IPv6" -#: dashboard/templates/dashboard/vm-detail/network.html:52 +#: dashboard/templates/dashboard/vm-detail/network.html:54 msgid "Port access" msgstr "Portok elérése" -#: dashboard/templates/dashboard/vm-detail/network.html:119 +#: dashboard/templates/dashboard/vm-detail/network.html:121 msgid "This VM doesn't have an IPv6 address!" msgstr "A VM-nek nincs IPv6 címe." @@ -2226,11 +2319,11 @@ msgstr "Erőforrások mentése" msgid "Stop your VM to change resources." msgstr "Állítsa le a VM-et az erőforrások módosításához." -#: dashboard/templates/dashboard/vm-detail/resources.html:57 +#: dashboard/templates/dashboard/vm-detail/resources.html:51 msgid "Required traits" msgstr "Elvárt jellemzők" -#: dashboard/templates/dashboard/vm-detail/resources.html:69 +#: dashboard/templates/dashboard/vm-detail/resources.html:63 msgid "Raw data" msgstr "Nyers adat" @@ -2254,121 +2347,146 @@ msgstr "példányok száma" msgid "Allocated memory (bytes)" msgstr "Foglalt memória (byte)" -#: dashboard/views/group.py:140 +#: dashboard/views/group.py:150 #, python-format msgid "User \"%s\" not found." msgstr "Nem található „%s” felhasználó." -#: dashboard/views/group.py:154 +#: dashboard/views/group.py:164 msgid "Group successfully renamed." msgstr "A csoport átnevezésre került." -#: dashboard/views/group.py:261 +#: dashboard/views/group.py:268 msgid "Member successfully removed from group." msgstr "A csoporttag eltávolításra került." -#: dashboard/views/group.py:302 +#: dashboard/views/group.py:309 msgid "Future user successfully removed from group." msgstr "A leendő csoporttag eltávolításra került." -#: dashboard/views/group.py:329 +#: dashboard/views/group.py:336 msgid "Group successfully deleted." msgstr "A csoport törlésre került." -#: dashboard/views/group.py:369 +#: dashboard/views/group.py:376 msgid "Create a Group" msgstr "Csoport létrehozása" -#: dashboard/views/group.py:385 +#: dashboard/views/group.py:392 msgid "Group successfully created." msgstr "A csoport létrehozásra került." -#: dashboard/views/group.py:399 +#: dashboard/views/group.py:406 msgid "Group is successfully updated." msgstr "A csoport frissítésre került." -#: dashboard/views/node.py:112 +#: dashboard/views/node.py:114 msgid "Node successfully renamed." msgstr "A csomópont átnevezésre került." -#: dashboard/views/node.py:220 +#: dashboard/views/node.py:222 msgid "Node successfully created." msgstr "A csomópont létrehozásra került." -#: dashboard/views/node.py:248 +#: dashboard/views/node.py:250 msgid "Node successfully deleted." msgstr "A csomópont törlésre került." -#: dashboard/views/node.py:295 +#: dashboard/views/node.py:297 msgid "Trait successfully added to node." msgstr "A csomópontjellemző hozzáadásra került." -#: dashboard/views/node.py:340 +#: dashboard/views/node.py:342 msgid "Node successfully changed status." msgstr "A csomópont állapota megváltoztatásra került." -#: dashboard/views/store.py:72 +#: dashboard/views/store.py:73 msgid "No store." msgstr "Nincs tárhely." -#: dashboard/views/store.py:74 +#: dashboard/views/store.py:75 msgid "Store has some problems now. Try again later." msgstr "A tárhely nem működik. Próbálja később." -#: dashboard/views/store.py:78 +#: dashboard/views/store.py:79 msgid "Unknown store error." msgstr "Ismeretlen tárhelyhiba." -#: dashboard/views/store.py:95 +#: dashboard/views/store.py:96 msgid "Something went wrong during download." msgstr "Hiba a letöltésben." -#: dashboard/views/store.py:110 dashboard/views/store.py:130 +#: dashboard/views/store.py:111 dashboard/views/store.py:131 msgid "Unable to upload file." msgstr "Fájl feltöltése sikertelen." -#: dashboard/views/store.py:167 +#: dashboard/views/store.py:168 #, python-format msgid "Unable to remove %s." msgstr "%s törlése sikertelen." -#: dashboard/views/store.py:186 +#: dashboard/views/store.py:187 msgid "Unable to create folder." msgstr "Mappa létrehozása sikertelen." -#: dashboard/views/template.py:64 +#: dashboard/views/template.py:65 msgid "Choose template" msgstr "Válasszon sablont" -#: dashboard/views/template.py:79 +#: dashboard/views/template.py:80 msgid "Select an option to proceed." msgstr "Válasszon a folytatáshoz." -#: dashboard/views/template.py:110 +#: dashboard/views/template.py:111 msgid "Create a new base VM" msgstr "Alap VM létrehozása" -#: dashboard/views/template.py:225 +#: dashboard/views/template.py:226 msgid "Error during filtering." msgstr "A szűrés sikertelen." -#: dashboard/views/template.py:250 +#: dashboard/views/template.py:245 +msgid "Only the owners can delete the selected template." +msgstr "A kiválasztott sablont csak a tulajdonosok törölhetik." + +#: dashboard/views/template.py:261 msgid "Template successfully deleted." msgstr "A sablon törlésre került." -#: dashboard/views/template.py:266 +#: dashboard/views/template.py:277 msgid "Successfully modified template." msgstr "A sablon módosításra került." -#: dashboard/views/template.py:327 +#: dashboard/views/template.py:352 +msgid "Disk remove confirmation" +msgstr "Lemez törlésének megerősítése" + +#: dashboard/views/template.py:353 +#, python-format +msgid "" +"Are you sure you want to remove %(disk)s from " +"%(app)s?" +msgstr "" +"Biztosan eltávolítja a(z) %(disk)s lemezt a következőből: " +"%(app)s?" + +#: dashboard/views/template.py:372 +msgid "Disk successfully removed." +msgstr "A lemez eltávolításra került." + +#: dashboard/views/template.py:390 msgid "Successfully created a new lease." msgstr "Új bérlési mód létrehozásra került." -#: dashboard/views/template.py:342 +#: dashboard/views/template.py:409 msgid "Successfully modified lease." msgstr "A bérlési mód megváltoztatásra került." -#: dashboard/views/template.py:372 +#: dashboard/views/template.py:423 +msgid "Only the owners can modify the selected lease." +msgstr "Csak a tulajdonosai törölhetik a kiválasztott bérleti módot." + +#: dashboard/views/template.py:452 msgid "" "You can't delete this lease because some templates are still using it, " "modify these to proceed: " @@ -2376,7 +2494,11 @@ msgstr "" "Nem törölhető a bérleti mód, mivel az alábbi sablonok még használják. A " "folytatáshoz módosítsa őket: " -#: dashboard/views/template.py:389 +#: dashboard/views/template.py:463 +msgid "Only the owners can delete the selected lease." +msgstr "Csak a tulajdonos törölheti a kiválasztott bérleti módot." + +#: dashboard/views/template.py:481 msgid "Lease successfully deleted." msgstr "A bérlési mód törlésre került." @@ -2393,27 +2515,27 @@ msgstr "Nincs profilja." msgid "Successfully modified subscription." msgstr "A feliratkozás módosításra került." -#: dashboard/views/user.py:350 +#: dashboard/views/user.py:369 msgid "Successfully modified SSH key." msgstr "Az SSH kulcs módosításra került." -#: dashboard/views/user.py:388 +#: dashboard/views/user.py:407 msgid "SSH key successfully deleted." msgstr "Az SSH kulcs törlésre került." -#: dashboard/views/user.py:404 +#: dashboard/views/user.py:423 msgid "Successfully created a new SSH key." msgstr "Az új SSH kulcs hozzáadásra került." -#: dashboard/views/user.py:420 +#: dashboard/views/user.py:439 msgid "Successfully modified command template." msgstr "A parancssablon módosításra került." -#: dashboard/views/user.py:463 +#: dashboard/views/user.py:482 msgid "Command template successfully deleted." msgstr "A parancssablon törlésre került." -#: dashboard/views/user.py:480 +#: dashboard/views/user.py:499 msgid "Successfully created a new command template." msgstr "A parancssablon létrehozásra került." @@ -2448,170 +2570,153 @@ msgstr "A(z) %(w)s ACL felhasználó/csoport hozzáadásra került." msgid "Acl user/group %(w)s successfully removed." msgstr "A(z) %(w)s ACL felhasználó/csoport törlésre került." -#: dashboard/views/util.py:474 +#: dashboard/views/util.py:475 msgid "" "The original owner cannot be removed, however you can transfer ownership." msgstr "Az eredeti tulajdonos nem törölhető, azonban a tulajdon átruházható." -#: dashboard/views/util.py:510 +#: dashboard/views/util.py:511 #, python-format msgid "User \"%s\" has already access to this object." msgstr "„%s” felhasználó már hozzáfér az objektumhoz." -#: dashboard/views/util.py:519 +#: dashboard/views/util.py:520 #, python-format msgid "Group \"%s\" has already access to this object." msgstr "„%s” csoport már hozzáfér az objektumhoz." -#: dashboard/views/util.py:524 +#: dashboard/views/util.py:525 #, python-format msgid "User or group \"%s\" not found." msgstr "Nem található „%s” felhasználó vagy csoport." -#: dashboard/views/util.py:540 +#: dashboard/views/util.py:541 msgid "1 hour" msgstr "1 óra" -#: dashboard/views/util.py:541 +#: dashboard/views/util.py:542 msgid "6 hours" msgstr "6 óra" -#: dashboard/views/util.py:542 +#: dashboard/views/util.py:543 msgid "1 day" msgstr "1 nap" -#: dashboard/views/util.py:543 +#: dashboard/views/util.py:544 msgid "1 week" msgstr "1 hét" -#: dashboard/views/util.py:544 +#: dashboard/views/util.py:545 msgid "1 month" msgstr "1 hónap" -#: dashboard/views/util.py:545 +#: dashboard/views/util.py:546 msgid "6 months" msgstr "6 hónap" -#: dashboard/views/util.py:554 +#: dashboard/views/util.py:555 msgid "Bad graph time format, available periods are: h, d, w, and y." msgstr "Hibás grafikon időformátum. Lehetséges egységek: h, d, w és y." -#: dashboard/views/vm.py:82 +#: dashboard/views/vm.py:84 msgid "console access" msgstr "konzolhozzáférés" -#: dashboard/views/vm.py:183 +#: dashboard/views/vm.py:193 msgid "VM successfully renamed." msgstr "A virtuális gép átnevezésre került." -#: dashboard/views/vm.py:207 +#: dashboard/views/vm.py:217 msgid "VM description successfully updated." msgstr "A VM leírása megváltoztatásra került." -#: dashboard/views/vm.py:284 +#: dashboard/views/vm.py:294 msgid "There is a problem with your input." msgstr "A megadott érték nem megfelelő." -#: dashboard/views/vm.py:286 +#: dashboard/views/vm.py:296 msgid "Unknown error." msgstr "Ismeretlen hiba." -#: dashboard/views/vm.py:491 +#: dashboard/views/vm.py:543 msgid "The token has expired." msgstr "A token lejárt." -#: dashboard/views/vm.py:676 +#: dashboard/views/vm.py:757 #, python-format msgid "Failed to execute %(op)s operation on instance %(instance)s." msgstr "%(op)s végrehajtása meghiúsult a következőn: %(instance)s." -#: dashboard/views/vm.py:692 +#: dashboard/views/vm.py:773 #, python-format msgid "You are not permitted to execute %(op)s on instance %(instance)s." msgstr "Nem engedélyezett a(z) %(op)s végrehajtása a(z) %(instance)s gépen." -#: dashboard/views/vm.py:868 +#: dashboard/views/vm.py:955 msgid "Customize VM" msgstr "VM testreszabása" -#: dashboard/views/vm.py:876 +#: dashboard/views/vm.py:963 msgid "Create a VM" msgstr "VM létrehozása" -#: dashboard/views/vm.py:941 +#: dashboard/views/vm.py:1028 #, python-format msgid "Successfully created %(count)d VM." msgid_plural "Successfully created %(count)d VMs." msgstr[0] "%(count)d VM létrehozásra került." msgstr[1] "%(count)d VM létrehozásra került." -#: dashboard/views/vm.py:946 +#: dashboard/views/vm.py:1033 msgid "VM successfully created." msgstr "VM létrehozásra került." -#: dashboard/views/vm.py:975 +#: dashboard/views/vm.py:1062 #, python-format msgid "Instance limit (%d) exceeded." msgstr "A példányok létrehozási korlátját (%d) túllépte." -#: dashboard/views/vm.py:1013 +#: dashboard/views/vm.py:1100 #, python-format msgid "" "Are you sure you want to remove this interface from %(vm)s?" msgstr "" "Biztosan eltávolítja az interfészt a(z) %(vm)s gépből?" -#: dashboard/views/vm.py:1027 +#: dashboard/views/vm.py:1114 msgid "Interface successfully deleted." msgstr "Az interfész törlésre került." -#: dashboard/views/vm.py:1080 -msgid "Disk remove confirmation" -msgstr "Lemez törlésének megerősítése" - -#: dashboard/views/vm.py:1081 -#, python-format -msgid "" -"Are you sure you want to remove %(disk)s from " -"%(app)s?" -msgstr "" -"Biztosan eltávolítja a(z) %(disk)s lemezt a következőből: " -"%(app)s?" - -#: dashboard/views/vm.py:1100 -msgid "Disk successfully removed." -msgstr "A lemez eltávolításra került." - -#: dashboard/views/vm.py:1141 +#: dashboard/views/vm.py:1183 msgid "Port delete confirmation" msgstr "Porteltávolítás megerősítése" -#: dashboard/views/vm.py:1142 +#: dashboard/views/vm.py:1184 #, python-format msgid "Are you sure you want to close %(port)d/%(proto)s on %(vm)s?" msgstr "Biztosan bezárja a(z) %(port)d/%(proto)s portot a következőn: %(vm)s?" -#: dashboard/views/vm.py:1157 +#: dashboard/views/vm.py:1199 msgid "Port successfully removed." msgstr "A port eltávolításra került." -#: dashboard/views/vm.py:1184 +#: dashboard/views/vm.py:1226 msgid "About CIRCLE Client" msgstr "A CIRCLE kliensről" -#: dashboard/views/vm.py:1281 +#: dashboard/views/vm.py:1323 msgid "Transfer ownership" msgstr "Tulajdon átruházása" -#: dashboard/views/vm.py:1294 +#: dashboard/views/vm.py:1336 msgid "Can not find specified user." msgstr "Nem található a megadott felhasználó." -#: dashboard/views/vm.py:1310 +#: dashboard/views/vm.py:1352 msgid "Ownership offer" msgstr "Átruházási ajánlat" -#: dashboard/views/vm.py:1311 +#: dashboard/views/vm.py:1353 #, python-format msgid "" "%(user)s offered you to take the ownership of his/her virtual machine called " @@ -2621,32 +2726,32 @@ msgstr "" "%(user)s át kívánja ruházni %(instance)s nevű virtuális gépét Önre. Elfogadás" -#: dashboard/views/vm.py:1317 +#: dashboard/views/vm.py:1359 msgid "Can not notify selected user." msgstr "A kiválaszott felhasználó értesítése sikertelen." -#: dashboard/views/vm.py:1320 +#: dashboard/views/vm.py:1362 #, python-format msgid "User %s is notified about the offer." msgstr "%s felhasználó értesítésre került az ajánlatról." -#: dashboard/views/vm.py:1331 +#: dashboard/views/vm.py:1373 msgid "Ownership successfully transferred to you." msgstr "A tulajdon átruházásra került." -#: dashboard/views/vm.py:1344 +#: dashboard/views/vm.py:1386 msgid "This token is for an other user." msgstr "A token más felhasználó nevére szól." -#: dashboard/views/vm.py:1347 +#: dashboard/views/vm.py:1389 msgid "This token is invalid or has expired." msgstr "A token érvénytelen vagy lejárt." -#: dashboard/views/vm.py:1370 +#: dashboard/views/vm.py:1411 msgid "Ownership accepted" msgstr "Átruházás elfogadva" -#: dashboard/views/vm.py:1371 +#: dashboard/views/vm.py:1412 #, python-format msgid "Your ownership offer of %(instance)s has been accepted by %(user)s." msgstr "%(instance)s gépre vonatkozó átruházási ajánlatát elfogadta %(user)s." @@ -2728,8 +2833,8 @@ msgstr "A szabály kimenő vagy bejövő csomagokra illeszkedik." #: firewall/models.py:68 firewall/models.py:334 firewall/models.py:419 #: firewall/models.py:442 firewall/models.py:499 firewall/models.py:857 -#: firewall/models.py:881 firewall/models.py:951 vm/models/instance.py:146 -#: vm/models/instance.py:236 +#: firewall/models.py:881 firewall/models.py:951 vm/models/instance.py:137 +#: vm/models/instance.py:227 msgid "description" msgstr "leírás" @@ -2828,8 +2933,8 @@ msgstr "módosítva" #: firewall/models.py:124 firewall/models.py:507 #: network/templates/network/vlan-create.html:8 -#: network/templates/network/vlan-edit.html:8 vm/models/network.py:40 -#: vm/models/network.py:65 +#: network/templates/network/vlan-edit.html:8 vm/models/network.py:39 +#: vm/models/network.py:64 msgid "vlan" msgstr "vlan" @@ -2848,7 +2953,7 @@ msgstr "Erre a vlan-csoportra vonatkozik a szabály (ha a típus vlan)." #: firewall/models.py:133 firewall/models.py:874 firewall/models.py:994 #: network/templates/network/host-create.html:8 -#: network/templates/network/host-edit.html:8 vm/models/network.py:67 +#: network/templates/network/host-edit.html:8 vm/models/network.py:66 #: vm/models/node.py:70 msgid "host" msgstr "gép" @@ -2968,7 +3073,7 @@ msgstr "" msgid "network type" msgstr "hálózat típusa" -#: firewall/models.py:333 vm/models/network.py:42 +#: firewall/models.py:333 vm/models/network.py:41 msgid "managed" msgstr "menedzselt" @@ -3788,7 +3893,7 @@ msgstr "eszközazonosító" msgid "disk" msgstr "lemez" -#: storage/models.py:104 vm/models/instance.py:151 vm/models/instance.py:257 +#: storage/models.py:104 vm/models/instance.py:142 vm/models/instance.py:248 msgid "disks" msgstr "lemezek" @@ -3800,77 +3905,81 @@ msgstr "Létrehozhat új lemezt." msgid "Can download a disk." msgstr "Letölthet lemezt." -#: storage/models.py:120 +#: storage/models.py:108 +msgid "Can resize a disk." +msgstr "Átméretezhet lemezt." + +#: storage/models.py:122 #, python-format msgid "Operation can't be invoked on disk '%(name)s' of type '%(type)s'." msgstr "" -"A kér művelet nem hajtható végre a(z) %(type)s típusú „%(name)s” lemezen." +"A kért művelet nem hajtható végre a(z) %(type)s típusú „%(name)s” lemezen." -#: storage/models.py:124 +#: storage/models.py:126 #, python-format msgid "" "Operation can't be invoked on disk '%(name)s' (%(pk)s) of type '%(type)s'." msgstr "" -"A kér művelet nem hajtható végre a(z) %(type)s típusú „%(name)s” (%(pk)s) " +"A kért művelet nem hajtható végre a(z) %(type)s típusú „%(name)s” (%(pk)s) " "lemezen." -#: storage/models.py:133 +#: storage/models.py:135 #, python-format msgid "" "The requested operation can't be performed on disk '%(name)s' because it is " "in use." msgstr "" -"A kér művelet nem hajtható végre a(z) „%(name)s” lemezen, mivel használatban " +"A kért művelet nem hajtható végre a(z) „%(name)s” lemezen, mivel használatban " "van." -#: storage/models.py:137 +#: storage/models.py:139 #, python-format msgid "" "The requested operation can't be performed on disk '%(name)s' (%(pk)s) " "because it is in use." msgstr "" -"A kér művelet nem hajtható végre a(z) „%(name)s” (%(pk)s) lemezen, mivel " +"A kért művelet nem hajtható végre a(z) „%(name)s” (%(pk)s) lemezen, mivel " "használatban van." -#: storage/models.py:146 +#: storage/models.py:148 #, python-format msgid "" "The requested operation can't be performed on disk '%(name)s' because it has " "never been deployed." msgstr "" -"A kér művelet nem hajtható végre a(z) „%(name)s” lemezen, mivel nem volt még " +"A kért művelet nem hajtható végre a(z) „%(name)s” lemezen, mivel nem volt még " "csatolva." -#: storage/models.py:150 +#: storage/models.py:152 #, python-format msgid "" "The requested operation can't be performed on disk '%(name)s' (%(pk)s) " "[%(filename)s] because it has never beendeployed." msgstr "" -"A kér művelet nem hajtható végre a(z) „%(name)s” (%(pk)s) [%(filename)s] " +"A kért művelet nem hajtható végre a(z) „%(name)s” (%(pk)s) [%(filename)s] " "lemezen, mivel nem volt még csatolva." -#: storage/models.py:161 +#: storage/models.py:163 #, python-format msgid "" "The requested operation can't be performed on disk '%(name)s' because its " "base has never been deployed." msgstr "" -"A kér művelet nem hajtható végre a(z) „%(name)s” lemezen, mivel az alapja " +"A kért művelet nem hajtható végre a(z) „%(name)s” lemezen, mivel az alapja " "nem volt még csatolva." -#: storage/models.py:165 +#: storage/models.py:167 #, python-format msgid "" "The requested operation can't be performed on disk '%(name)s' (%(pk)s) " "[%(filename)s] because its base '%(b_name)s' (%(b_pk)s) [%(b_filename)s] has " "never beendeployed." msgstr "" -"A kér művelet nem hajtható végre a(z) „%(name)s” (%(pk)s) [%(filename)s] " +"A kért művelet nem hajtható végre a(z) „%(name)s” (%(pk)s) [%(filename)s] " "lemezen, mivel az alapja, „%(b_name)s” (%(b_pk)s) [%(b_filename)s] nem " "volt még csatolva." -#: storage/models.py:414 storage/models.py:489 vm/models/instance.py:890 +#: storage/models.py:416 storage/models.py:493 vm/operations.py:92 msgid "Operation aborted by user." msgstr "A műveletet a felhasználó megszakította." @@ -4518,59 +4627,68 @@ msgstr "Jelszó visszaállítása" msgid "Enter your email address to reset your password." msgstr "Adja meg e-mail címét a jelszó visszaállításához." -#: vm/operations.py:86 +#: vm/operations.py:129 #, python-format msgid "%(acl_level)s level is required for this operation." msgstr "%(acl_level)s jogosultság szükséges a művelethez." -#: vm/operations.py:124 +#: vm/operations.py:202 msgid "Add a new network interface for the specified VLAN to the VM." msgstr "Új hálózati interfész hozzáadása a megadott VLAN-ba." -#: vm/operations.py:132 +#: vm/operations.py:210 msgid "destroy network (rollback)" msgstr "hálózat megsemmisítése (visszagörgetés)" -#: vm/operations.py:139 +#: vm/operations.py:217 #, python-format msgid "User acces to vlan %(vlan)s is required." msgstr "Használói jogosultság szükséges a(z) %(vlan)s vlan-hoz." -#: vm/operations.py:151 -msgid "attach network" -msgstr "hálózat csatolása" - -#: vm/operations.py:161 +#: vm/operations.py:238 #, python-format msgid "add %(vlan)s interface" msgstr "új %(vlan)s interfész" -#: vm/operations.py:170 +#: vm/operations.py:246 msgid "create disk" msgstr "lemez létrehozása" -#: vm/operations.py:171 +#: vm/operations.py:247 msgid "Create and attach empty disk to the virtual machine." msgstr "Üres lemez létehozása és virtuális géphez csatolása." -#: vm/operations.py:192 +#: vm/operations.py:268 msgid "deploying disk" msgstr "lemez létrehozása" -#: vm/operations.py:197 vm/operations.py:241 -msgid "attach disk" -msgstr "lemez csatolása" - -#: vm/operations.py:203 +#: vm/operations.py:275 #, python-format msgid "create disk %(name)s (%(size)s)" msgstr "%(name)s lemez létrehozása (%(size)s)" -#: vm/operations.py:211 +#: vm/operations.py:283 +msgid "resize disk" +msgstr "lemez átméretezése" + +#: vm/operations.py:284 +msgid "" +"Resize the virtual disk image. Size must be greater value than the actual " +"size." +msgstr "" +"Virtuális lemezkép átméretezése. Az új méret meg kell haladja " +"a jelenlegit." + +#: vm/operations.py:298 +#, python-format +msgid "resize disk %(name)s to %(size)s" +msgstr "%(name)s lemez átméretezése (%(size)s)" + +#: vm/operations.py:310 msgid "download disk" msgstr "lemez letöltése" -#: vm/operations.py:212 +#: vm/operations.py:311 msgid "" "Download and attach disk image (ISO file) for the virtual machine. Most " "operating systems do not detect a new optical drive, so you may have to " @@ -4580,16 +4698,21 @@ msgstr "" "operációs rendszer nem érzékeli az új optikai meghajtót, így valószínűleg " "újra kell indítania a virtuális gépet." -#: vm/operations.py:235 +#: vm/operations.py:333 #, python-format msgid "download %(name)s" msgstr "%(name)s letöltése" -#: vm/operations.py:250 +#: vm/operations.py:336 +#, python-format +msgid "Downloading %(url)s is finished. The file md5sum is: '%(checksum)s'." +msgstr "%(url)s letöltése sikeres. A fájl md5sum összege: '%(checksum)s'." + +#: vm/operations.py:347 msgid "deploy" msgstr "indítás" -#: vm/operations.py:251 +#: vm/operations.py:348 msgid "" "Deploy and start the virtual machine (including storage and network " "configuration)." @@ -4597,101 +4720,117 @@ msgstr "" "Virtuális gép elhelyezése és indítása (valamint a lemezek és a hálózat " "beállítása)." -#: vm/operations.py:268 +#: vm/operations.py:365 #, python-format msgid "virtual machine successfully deployed to node: %(node)s" msgstr "a virtuális gép sikeresen elindítva a következő csomóponton: %(node)s" -#: vm/operations.py:280 -msgid "deploy disks" -msgstr "lemez létrehozása" +#: vm/operations.py:388 vm/operations.py:563 vm/operations.py:889 +msgid "deploy network" +msgstr "hálózati kapcsolat létrehozása" + +#: vm/operations.py:400 vm/operations.py:581 vm/operations.py:645 +msgid "wait operating system loading" +msgstr "várakozás az operációs rendszer betöltésére" + +#: vm/operations.py:405 +msgid "deploy vm" +msgstr "vm indítása" + +#: vm/operations.py:406 +msgid "Deploy virtual machine." +msgstr "Virtuális gép létrehozása." -#: vm/operations.py:285 +#: vm/operations.py:415 msgid "deploy virtual machine" msgstr "virtuális gép létrehozása" -#: vm/operations.py:286 +#: vm/operations.py:416 #, python-format msgid "deploy vm to %(node)s" msgstr "vm létrehozása: %(node)s" -#: vm/operations.py:295 vm/operations.py:408 vm/operations.py:734 -msgid "deploy network" -msgstr "hálózati kapcsolat létrehozása" +#: vm/operations.py:422 +msgid "deploy disks" +msgstr "lemez létrehozása" + +#: vm/operations.py:423 +msgid "Deploy all associated disks." +msgstr "Csatolt lemezek létrehozása." -#: vm/operations.py:306 +#: vm/operations.py:440 msgid "boot virtual machine" msgstr "virtuális gép indítása" -#: vm/operations.py:311 vm/operations.py:426 vm/operations.py:498 -msgid "wait operating system loading" -msgstr "várakozás az operációs rendszer betöltésére" - -#: vm/operations.py:318 +#: vm/operations.py:448 msgid "destroy" msgstr "megsemmisítés" -#: vm/operations.py:319 +#: vm/operations.py:449 msgid "Permanently destroy virtual machine, its network settings and disks." msgstr "" "Virtuális gép és lemezeinek, hálózati beállításainak végleges eltávolítása." -#: vm/operations.py:328 +#: vm/operations.py:458 msgid "destroy network" msgstr "hálózat megsemmisítése" -#: vm/operations.py:337 +#: vm/operations.py:469 +msgid "destroy disks" +msgstr "lemez megsemmisítése" + +#: vm/operations.py:488 msgid "destroy virtual machine" msgstr "virtuális gép megsemmisítése" -#: vm/operations.py:343 -msgid "destroy disks" -msgstr "lemez megsemmisítése" +#: vm/operations.py:496 +msgid "removing memory dump" +msgstr "memóriamentés törlése" -#: vm/operations.py:364 +#: vm/operations.py:510 msgid "migrate" msgstr "migrálás" -#: vm/operations.py:365 +#: vm/operations.py:511 msgid "" -"Move virtual machine to an other worker node with a few seconds of " -"interruption (live migration)." +"Move a running virtual machine to an other worker node keeping its full " +"state." msgstr "" -"A virtuális gép áthelyezése egy másik számítási csomópontra néhány másodperc " -"kimaradással (live migration)." +"A virtuális gép mozgatása egy másik számítási csomópontra állapotának " +"megtartásával." -#: vm/operations.py:375 +#: vm/operations.py:528 msgid "redeploy network (rollback)" msgstr "hálózati kapcsolat újraépítése (visszagörgetés)" -#: vm/operations.py:382 +#: vm/operations.py:535 msgid "schedule" msgstr "ütemezés" -#: vm/operations.py:389 +#: vm/operations.py:542 #, python-format msgid "migrate to %(node)s" msgstr "migrálás %(node)s csomópontra" -#: vm/operations.py:399 vm/operations.py:686 +#: vm/operations.py:553 vm/operations.py:838 msgid "shutdown network" msgstr "hálózati kapcsolat leállítása" -#: vm/operations.py:416 +#: vm/operations.py:570 msgid "reboot" msgstr "újraindítás" -#: vm/operations.py:417 +#: vm/operations.py:571 msgid "" "Warm reboot virtual machine by sending Ctrl+Alt+Del signal to its console." msgstr "" "Virtuális gép újraindítása a konzoljára a Ctrl+Alt+Del kombináció küldésével." -#: vm/operations.py:433 +#: vm/operations.py:587 msgid "remove interface" msgstr "interfész törlése" -#: vm/operations.py:434 +#: vm/operations.py:588 msgid "" "Remove the specified network interface and erase IP address allocations, " "related firewall rules and hostnames." @@ -4699,50 +4838,42 @@ msgstr "" "A kiválasztott hálózati interfész eltávolítása, a foglalt IP címek, " "tűzfalszabályok és gépnevek törlése." -#: vm/operations.py:444 -msgid "detach network" -msgstr "hálózat lecsatolása" - -#: vm/operations.py:453 +#: vm/operations.py:604 #, python-format msgid "remove %(vlan)s interface" msgstr "%(vlan)s interfész törlése" -#: vm/operations.py:461 +#: vm/operations.py:611 msgid "remove disk" msgstr "lemez eltávolítása" -#: vm/operations.py:462 +#: vm/operations.py:612 msgid "" "Remove the specified disk from the virtual machine, and destroy the data." msgstr "A megadott lemez eltávolítása a virtuális gépből és az adat törlése." -#: vm/operations.py:471 -msgid "detach disk" -msgstr "lemez leválasztása" - -#: vm/operations.py:476 +#: vm/operations.py:622 msgid "destroy disk" msgstr "lemez megsemmisítése" -#: vm/operations.py:481 +#: vm/operations.py:628 #, python-format msgid "remove disk %(name)s" msgstr "%(name)s lemez eltávolítása" -#: vm/operations.py:489 +#: vm/operations.py:635 vm/operations.py:1041 msgid "reset" msgstr "reset" -#: vm/operations.py:490 +#: vm/operations.py:636 msgid "Cold reboot virtual machine (power cycle)." msgstr "Virtuális gép hideg újraindítása (hálózati tápellátás megszakítása)." -#: vm/operations.py:505 +#: vm/operations.py:651 msgid "save as template" msgstr "mentés sablonként" -#: vm/operations.py:506 +#: vm/operations.py:652 msgid "" "Save virtual machine as a template so they can be shared with users and " "groups. Anyone who has access to a template (and to the networks it uses) " @@ -4752,16 +4883,16 @@ msgstr "" "felhasználókkal és csoportokkal. Mindenki, aki hozzáférést kap egy sablonhoz " "(és az általa használt hálózatokhoz), képes lesz egy példányát elindítani." -#: vm/operations.py:576 +#: vm/operations.py:728 #, python-format msgid "saving disk %(name)s" msgstr "%(name)s lemez mentése" -#: vm/operations.py:601 +#: vm/operations.py:755 msgid "shutdown" msgstr "leállítás" -#: vm/operations.py:602 +#: vm/operations.py:756 msgid "" "Try to halt virtual machine by a standard ACPI signal, allowing the " "operating system to keep a consistent state. The operation will fail if the " @@ -4771,7 +4902,7 @@ msgstr "" "operációs rendszer számár a szabályos leállást. A művelet meghiúsul, ha a " "gép nem áll le." -#: vm/operations.py:618 +#: vm/operations.py:775 msgid "" "The virtual machine did not switch off in the provided time limit. Most of " "the time this is caused by incorrect ACPI settings. You can also try to " @@ -4781,11 +4912,11 @@ msgstr "" "ez a nem megfelelő ACPI beállítások miatt van. Megpróbálhatja a gépet az " "operációs rendszerből, kézzel leállítani." -#: vm/operations.py:631 +#: vm/operations.py:787 msgid "shut off" msgstr "kikapcsolás" -#: vm/operations.py:632 +#: vm/operations.py:788 msgid "" "Forcibly halt a virtual machine without notifying the operating system. This " "operation will even work in cases when shutdown does not, but the operating " @@ -4798,11 +4929,11 @@ msgstr "" "rendszer és a fájlrendszer sérülhet, adatvesztés történhet. A művelet hatása " "hasonló, mint egy fizikai gép tápellátásának megszüntetése." -#: vm/operations.py:659 +#: vm/operations.py:811 msgid "sleep" msgstr "altatás" -#: vm/operations.py:660 +#: vm/operations.py:812 msgid "" "Suspend virtual machine. This means the machine is stopped and its memory is " "saved to disk, so if the machine is waked up, all the applications will keep " @@ -4818,15 +4949,15 @@ msgstr "" "megállhatnak visszaállítás után. A felfüggesztés ideje alatt a virtuális gép " "csak tárterületet és hálózati erőforrásokat foglal." -#: vm/operations.py:692 +#: vm/operations.py:846 msgid "suspend virtual machine" msgstr "virtuális gép felfüggesztése" -#: vm/operations.py:703 +#: vm/operations.py:860 msgid "wake up" msgstr "virtuális gép ébresztése" -#: vm/operations.py:704 +#: vm/operations.py:861 msgid "" "Wake up sleeping (suspended) virtual machine. This will load the saved " "memory of the system and start the virtual machine from this state." @@ -4834,15 +4965,15 @@ msgstr "" "Alvó (felfüggesztett) gép ébresztése: az elmentett memóriatartalom " "visszatöltése és a virtuális gép indítása ebből a mentett állapotból." -#: vm/operations.py:728 +#: vm/operations.py:900 msgid "resume virtual machine" msgstr "virtuális gép ébresztése" -#: vm/operations.py:747 +#: vm/operations.py:914 msgid "renew" msgstr "megújítás" -#: vm/operations.py:748 +#: vm/operations.py:915 msgid "" "Virtual machines are suspended and destroyed after they expire. This " "operation renews expiration times according to the lease type. If the " @@ -4852,7 +4983,7 @@ msgstr "" "a művelet megújítja a bérletet a kiválasztott típusnak megfelelően. Ha egy " "gép közeledik a lejárathoz, a tulajdonost értesítjük." -#: vm/operations.py:761 +#: vm/operations.py:928 msgid "" "Renewing the machine with the selected lease would result in its suspension " "time get earlier than before." @@ -4860,7 +4991,7 @@ msgstr "" "A gép megújítása a kiválasztott bérleti mód mellett a felfüggesztési időt " "korábbra állította volna, mint a jelenlegi érték." -#: vm/operations.py:766 +#: vm/operations.py:933 msgid "" "Renewing the machine with the selected lease would result in its delete time " "get earlier than before." @@ -4868,17 +4999,17 @@ msgstr "" "A gép megújítása a kiválasztott bérleti mód mellett a törlési időt korábbra " "állította volna, mint a jelenlegi érték." -#: vm/operations.py:774 +#: vm/operations.py:941 #, python-format msgid "Renewed to suspend at %(suspend)s and destroy at %(delete)s." msgstr "" "Megújítás után felfüggesztés ideje: %(suspend)s, a törlésé: %(delete)s." -#: vm/operations.py:782 +#: vm/operations.py:948 msgid "emergency state change" msgstr "vész-állapotváltás" -#: vm/operations.py:783 +#: vm/operations.py:949 msgid "" "Change the virtual machine state to NOSTATE. This should only be used if " "manual intervention was needed in the virtualization layer, and the machine " @@ -4889,46 +5020,68 @@ msgstr "" "rétegben, és úgy szeretné a gépet újból elindítani, hogy ne vesszenek el " "lemezei vagy hálózati erőforrásai." -#: vm/operations.py:795 +#: vm/operations.py:962 msgid "Activity is forcibly interrupted." msgstr "A tevékenység erőszakos megszakításra került." -#: vm/operations.py:817 +#: vm/operations.py:977 +msgid "redeploy" +msgstr "újbóli létrehozás" + +#: vm/operations.py:978 +msgid "" +"Change the virtual machine state to NOSTATE and redeploy the VM. This " +"operation allows starting machines formerly running on a failed node." +msgstr "" +"A virtuális gép állapotának átállítása NOSTATE-re, majd a VM újbóli " +"létrehozása. " +"Ez a művelet lehetővé teszi olyan gépek elindítását, amelyek korábban egy " +"meghibásodott csomóponton futnak." + +#: vm/operations.py:1014 msgid "You cannot call this operation on an offline node." msgstr "Nem hívható ez a művelet elérhetetlen csomópontra." -#: vm/operations.py:844 +#: vm/operations.py:1042 +msgid "Disable missing node and redeploy all instances on other ones." +msgstr "Hiányzó csomópont letiltása és az összes példány elindítása a többin." + +#: vm/operations.py:1052 +msgid "You cannot reset a disabled or online node." +msgstr "Tiltott vagy elérhető csomópont resetelése nem lehetséges." + +#: vm/operations.py:1060 vm/operations.py:1080 +#, python-format +msgid "migrate %(instance)s (%(pk)s)" +msgstr "%(instance)s (%(pk)s) migrálása" + +#: vm/operations.py:1069 msgid "flush" msgstr "ürítés" -#: vm/operations.py:845 +#: vm/operations.py:1070 msgid "Passivate node and move all instances to other ones." msgstr "" "A csomópont passzívra állítása és az összes példány másikakra mozgatása." -#: vm/operations.py:855 -#, python-format -msgid "migrate %(instance)s (%(pk)s)" -msgstr "%(instance)s (%(pk)s) migrálása" - -#: vm/operations.py:865 +#: vm/operations.py:1089 msgid "activate" msgstr "aktiválás" -#: vm/operations.py:866 +#: vm/operations.py:1090 msgid "" "Make node active, i.e. scheduler is allowed to deploy virtual machines to it." msgstr "Csomópont aktívvá tétele: az ütemező indíthat virtuális gépeket rajta." -#: vm/operations.py:874 +#: vm/operations.py:1098 msgid "You cannot activate an active node." msgstr "Aktív csomópont aktiválása nem lehetséges." -#: vm/operations.py:886 +#: vm/operations.py:1109 msgid "passivate" msgstr "passziválás" -#: vm/operations.py:887 +#: vm/operations.py:1110 msgid "" "Make node passive, i.e. scheduler is denied to deploy virtual machines to " "it, but remaining instances and the ones manually migrated will continue " @@ -4937,31 +5090,31 @@ msgstr "" "Csomópont passzívvá tétele: az ütemező nem indíthat rajta virtuális gépeket, " "azonban a megmaradt példányok és a kézzel idemigráltak tovább működnek." -#: vm/operations.py:895 +#: vm/operations.py:1118 msgid "You cannot passivate a passive node." msgstr "Passzív csomópont passziválása nem lehetséges." -#: vm/operations.py:908 +#: vm/operations.py:1130 msgid "disable" msgstr "tiltás" -#: vm/operations.py:909 +#: vm/operations.py:1131 msgid "Disable node." msgstr "Csomópont tiltása." -#: vm/operations.py:916 +#: vm/operations.py:1138 msgid "You cannot disable a disabled node." msgstr "Tiltott csomópont tiltása nem lehetséges." -#: vm/operations.py:919 +#: vm/operations.py:1141 msgid "You cannot disable a node which is hosting instances." msgstr "Nem tiltható le olyan csomópont, amelyen még futnak példányok." -#: vm/operations.py:933 +#: vm/operations.py:1154 msgid "screenshot" msgstr "képernyőkép" -#: vm/operations.py:934 +#: vm/operations.py:1155 msgid "" "Get a screenshot about the virtual machine's console. A key will be pressed " "on the keyboard to stop screensaver." @@ -4969,11 +5122,11 @@ msgstr "" "Képernyőkép készítése a virtuális gép konzoljáról. Egy billentyűnyomást " "követően készül a kép a képernyővédő miatt." -#: vm/operations.py:949 +#: vm/operations.py:1167 msgid "recover" msgstr "visszaállítás" -#: vm/operations.py:950 +#: vm/operations.py:1168 msgid "" "Try to recover virtual machine disks from destroyed state. Network resources " "(allocations) are already lost, so you will have to manually add interfaces " @@ -4983,15 +5136,15 @@ msgstr "" "hálózati erőforrások foglalásai már végleg elvesztek, így az interfészeket " "kézzel kell a visszaállítás után pótolni." -#: vm/operations.py:977 +#: vm/operations.py:1194 msgid "resources change" msgstr "erőforrások módosítása" -#: vm/operations.py:978 +#: vm/operations.py:1195 msgid "Change resources of a stopped virtual machine." msgstr "Leállított virtuális gép erőforrásainak változtatása." -#: vm/operations.py:995 +#: vm/operations.py:1212 #, python-format msgid "" "Priority: %(priority)s, Num cores: %(num_cores)s, Ram size: %(ram_size)s" @@ -4999,11 +5152,11 @@ msgstr "" "Prioritás: %(priority)s, magok száma: %(num_cores)s, memória mérete: " "%(ram_size)s" -#: vm/operations.py:1024 +#: vm/operations.py:1221 msgid "password reset" msgstr "jelszó visszaállítása" -#: vm/operations.py:1025 +#: vm/operations.py:1222 msgid "" "Generate and set a new login password on the virtual machine. This operation " "requires the agent running. Resetting the password is not warranted to allow " @@ -5013,11 +5166,52 @@ msgstr "" "művelet megköveteli az ügynök futását. A jelszó átállítása nem garantálja a " "sikeres belépést, mivel más beállítások is megakadályozhatják ezt." -#: vm/operations.py:1045 +#: vm/operations.py:1246 +msgid "agent" +msgstr "ügynök" + +#: vm/operations.py:1287 +msgid "starting" +msgstr "indítás" + +#: vm/operations.py:1305 +msgid "wait agent restarting" +msgstr "várakozás az ügynök újraindulására" + +#: vm/operations.py:1322 +msgid "cleanup" +msgstr "takarítás" + +#: vm/operations.py:1328 +msgid "set time" +msgstr "óra beállítása" + +#: vm/operations.py:1339 +msgid "set hostname" +msgstr "gépnév beállítása" + +#: vm/operations.py:1350 +msgid "restart networking" +msgstr "hálózat újratöltése" + +#: vm/operations.py:1356 +msgid "change ip" +msgstr "IP cím beállítása" + +#: vm/operations.py:1371 +msgid "update agent" +msgstr "ügynök frissítése" + +#: vm/operations.py:1377 +#, python-format +msgid "update agent to %(version)s" +msgstr "ügynökfrissítés erre: %(version)s" + +#: vm/operations.py:1460 msgid "mount store" msgstr "tárhely csatolása" -#: vm/operations.py:1047 +#: vm/operations.py:1462 msgid "" "This operation attaches your personal file store. Other users who have " "access to this machine can see these files as well." @@ -5025,37 +5219,62 @@ msgstr "" "Ez a művelet csatolja az ön személyes tárhelyét. A gép más felhasználói is " "elérhetik fájljait." -#: vm/models/activity.py:47 +#: vm/operations.py:1496 +msgid "attach disk" +msgstr "lemez csatolása" + +#: vm/operations.py:1507 +msgid "Resource was not found." +msgstr "Nem található az erőforrás." + +#: vm/operations.py:1508 +#, python-format +msgid "Resource was not found. %(exception)s" +msgstr "Nem található az erőforrás. %(exception)s" + +#: vm/operations.py:1517 +msgid "detach disk" +msgstr "lemez leválasztása" + +#: vm/operations.py:1532 +msgid "attach network" +msgstr "hálózat csatolása" + +#: vm/operations.py:1539 +msgid "detach network" +msgstr "hálózat lecsatolása" + +#: vm/models/activity.py:46 #, python-format msgid "%(activity)s activity is currently in progress." msgstr "%(activity)s folyamatban van." -#: vm/models/activity.py:48 +#: vm/models/activity.py:47 #, python-format msgid "%(activity)s (%(pk)s) activity is currently in progress." msgstr "%(activity)s (%(pk)s) folyamatban van." -#: vm/models/activity.py:70 +#: vm/models/activity.py:69 msgid "Instance this activity works on." msgstr "A tevékenység tárgyát képező példány." -#: vm/models/activity.py:74 +#: vm/models/activity.py:73 msgid "Other activities can interrupt this one." msgstr "Más tevékenységek megszakíthatják ezt." -#: vm/models/activity.py:105 +#: vm/models/activity.py:104 msgid "Interrupted by other activity." msgstr "Egy másik tevékenység megszakította." -#: vm/models/activity.py:227 +#: vm/models/activity.py:211 msgid "Node this activity works on." msgstr "A tevékenység tárgyát képező csomópont." -#: vm/models/activity.py:228 +#: vm/models/activity.py:212 msgid "node" msgstr "csomópont" -#: vm/models/activity.py:286 +#: vm/models/activity.py:267 msgid "Manager is restarted, activity is cleaned up. You can try again now." msgstr "" "A menedzser újraindítása miatt a tevékenység lezárásra került. Próbálja újra." @@ -5125,60 +5344,60 @@ msgstr "soha" msgid "%(name)s (suspend: %(s)s, remove: %(r)s)" msgstr "%(name)s (felfüggesztés: %(s)s, törlés: %(r)s)" -#: vm/models/instance.py:108 +#: vm/models/instance.py:99 msgid "Primary remote access method." msgstr "Elsődleges távoli elérési mód." -#: vm/models/instance.py:109 +#: vm/models/instance.py:100 msgid "boot menu" msgstr "rendszerbetöltő menüje" -#: vm/models/instance.py:111 +#: vm/models/instance.py:102 msgid "Show boot device selection menu on boot." msgstr "" "A rendszerbetöltés eszközének kiválasztását lehetővé tevő menü megjelenítése " "indításkor." -#: vm/models/instance.py:112 +#: vm/models/instance.py:103 msgid "Preferred expiration periods." msgstr "Javasolt bérlési mód." -#: vm/models/instance.py:114 +#: vm/models/instance.py:105 msgid "raw_data" msgstr "nyers adat" -#: vm/models/instance.py:115 +#: vm/models/instance.py:106 msgid "Additional libvirt domain parameters in XML format." msgstr "További libvirt domain-paraméterek XML formátumban." -#: vm/models/instance.py:117 +#: vm/models/instance.py:108 msgid "" "A set of traits required for a node to declare to be suitable for hosting " "the VM." msgstr "A VM indításához szükséges csomópontjellemzők halmaza." -#: vm/models/instance.py:120 +#: vm/models/instance.py:111 msgid "required traits" msgstr "elvárt jellemzők" -#: vm/models/instance.py:121 +#: vm/models/instance.py:112 msgid "operating system" msgstr "operációs rendszer" -#: vm/models/instance.py:122 +#: vm/models/instance.py:113 #, python-format msgid "Name of operating system in format like \"%s\"." msgstr "Az operációs rendszer neve. Például „%s”." -#: vm/models/instance.py:125 vm/models/node.py:83 +#: vm/models/instance.py:116 vm/models/node.py:83 msgid "tags" msgstr "címkék" -#: vm/models/instance.py:126 +#: vm/models/instance.py:117 msgid "has agent" msgstr "van ügynöke" -#: vm/models/instance.py:128 +#: vm/models/instance.py:119 msgid "" "If the machine has agent installed, and the manager should wait for its " "start." @@ -5186,161 +5405,165 @@ msgstr "" "A gépre telepítve van-e az ügynökszoftver, vagyis a menedzser várjon-e az " "indulására." -#: vm/models/instance.py:148 +#: vm/models/instance.py:139 msgid "parent template" msgstr "szülősablon" -#: vm/models/instance.py:150 +#: vm/models/instance.py:141 msgid "Template which this one is derived of." msgstr "Az a sablon, amelyből az aktuális származik." -#: vm/models/instance.py:153 +#: vm/models/instance.py:144 msgid "Disks which are to be mounted." msgstr "A csatolandó lemezek." -#: vm/models/instance.py:161 +#: vm/models/instance.py:152 msgid "Can create an instance template." msgstr "Létrehozhat példánysablont." -#: vm/models/instance.py:163 +#: vm/models/instance.py:154 msgid "Can create an instance template (base)." msgstr "Létrehozhat példánysablont (alapokból)." -#: vm/models/instance.py:165 +#: vm/models/instance.py:156 msgid "Can change resources of a template." msgstr "Változtathatja egy sablon erőforrásait." -#: vm/models/instance.py:168 +#: vm/models/instance.py:159 msgid "templates" msgstr "sablonok" -#: vm/models/instance.py:226 +#: vm/models/instance.py:217 msgid "no state" msgstr "nincs állapot" -#: vm/models/instance.py:227 +#: vm/models/instance.py:218 msgid "running" msgstr "fut" -#: vm/models/instance.py:228 +#: vm/models/instance.py:219 msgid "stopped" msgstr "leállítva" -#: vm/models/instance.py:229 +#: vm/models/instance.py:220 msgid "suspended" msgstr "felfüggesztve" -#: vm/models/instance.py:230 +#: vm/models/instance.py:221 msgid "error" msgstr "hiba" -#: vm/models/instance.py:231 +#: vm/models/instance.py:222 msgid "pending" msgstr "függő" -#: vm/models/instance.py:232 +#: vm/models/instance.py:223 msgid "destroyed" msgstr "megsemmisítve" -#: vm/models/instance.py:235 +#: vm/models/instance.py:226 msgid "Human readable name of instance." msgstr "A példány olvasható neve." -#: vm/models/instance.py:239 +#: vm/models/instance.py:230 msgid "Template the instance derives from." msgstr "Az a sablon, amelyből a példány származik." -#: vm/models/instance.py:241 +#: vm/models/instance.py:232 msgid "Original password of the instance." msgstr "A példány eredeti jelszava." -#: vm/models/instance.py:242 +#: vm/models/instance.py:233 msgid "password" msgstr "jelszó" -#: vm/models/instance.py:244 +#: vm/models/instance.py:235 msgid "time of suspend" msgstr "felfüggesztés ideje" -#: vm/models/instance.py:245 +#: vm/models/instance.py:236 msgid "Proposed time of automatic suspension." msgstr "A felfüggesztés kijelölt ideje." -#: vm/models/instance.py:248 +#: vm/models/instance.py:239 msgid "time of delete" msgstr "törlés ideje" -#: vm/models/instance.py:249 +#: vm/models/instance.py:240 msgid "Proposed time of automatic deletion." msgstr "Automatikus törlés kijelölt ideje." -#: vm/models/instance.py:253 +#: vm/models/instance.py:244 msgid "Current hypervisor of this instance." msgstr "A példány jelenlegi hypervisorja." -#: vm/models/instance.py:254 +#: vm/models/instance.py:245 msgid "host node" msgstr "csomópont" -#: vm/models/instance.py:256 +#: vm/models/instance.py:247 msgid "Set of mounted disks." msgstr "1Csatolt lemezek halmaza." -#: vm/models/instance.py:259 +#: vm/models/instance.py:250 msgid "TCP port where VNC console listens." msgstr "Az a TCP port, amelyen a VNC konzol hallgat." -#: vm/models/instance.py:260 +#: vm/models/instance.py:251 msgid "vnc_port" msgstr "VNC port" -#: vm/models/instance.py:264 +#: vm/models/instance.py:255 msgid "The virtual machine's time of destruction." msgstr "A virtuális gép megsemmisítésének ideje." -#: vm/models/instance.py:274 +#: vm/models/instance.py:265 msgid "Can access the graphical console of a VM." msgstr "Elérheti a VM grafikus konzolját." -#: vm/models/instance.py:275 +#: vm/models/instance.py:266 msgid "Can change resources of a running VM." msgstr "Megváltoztathatja a VM erőforrásait." -#: vm/models/instance.py:276 +#: vm/models/instance.py:267 msgid "Can change resources of a new VM." msgstr "Megválaszthatja egy új VM erőforrásait." -#: vm/models/instance.py:277 +#: vm/models/instance.py:268 msgid "Can create a new VM." msgstr "Létrehozhat új VM-et." -#: vm/models/instance.py:278 +#: vm/models/instance.py:269 +msgid "Can redeploy a VM." +msgstr "Újból létrehozhat futó VM-et." + +#: vm/models/instance.py:270 msgid "Can configure port forwards." msgstr "Beállíthat porttovábbításokat." -#: vm/models/instance.py:279 +#: vm/models/instance.py:271 msgid "Can recover a destroyed VM." msgstr "Visszaállíthat egy megsemmisített VM-et." -#: vm/models/instance.py:280 +#: vm/models/instance.py:272 msgid "Can change VM state to NOSTATE." msgstr "Átállíthatja a VM állapotát NOSTATE-re." -#: vm/models/instance.py:283 +#: vm/models/instance.py:275 msgid "instances" msgstr "példányok" -#: vm/models/instance.py:295 +#: vm/models/instance.py:287 #, python-format msgid "Instance %(instance)s has already been destroyed." msgstr "%(instance)s példány már meg van semmisítve." -#: vm/models/instance.py:299 +#: vm/models/instance.py:291 #, python-format msgid "No agent software is running on instance %(instance)s." msgstr "Nem fut ügynökszoftver a következőn: %(instance)s." -#: vm/models/instance.py:303 +#: vm/models/instance.py:295 #, python-format msgid "" "Current state (%(state)s) of instance %(instance)s is inappropriate for the " @@ -5349,16 +5572,16 @@ msgstr "" "A(z) %(instance)s példány aktuális állapota (%(state)s) nem megfelelő a " "választott művelethez." -#: vm/models/instance.py:377 +#: vm/models/instance.py:369 msgid "create instance" msgstr "példány létrehozása" -#: vm/models/instance.py:458 +#: vm/models/instance.py:450 #, python-format msgid "vm state changed to %(state)s on %(node)s" -msgstr "VM állapota erre változott: %(state)s, %(node)s" +msgstr "VM állapota erre változott: %(state)s (ezen: %(node)s)" -#: vm/models/instance.py:654 +#: vm/models/instance.py:646 #, python-format msgid "" "Your instance %(instance)s is going to expire. It " @@ -5370,7 +5593,7 @@ msgstr "" "kerül. Kérjük, újítsa meg vagy törölje most." -#: vm/models/instance.py:666 +#: vm/models/instance.py:658 #, python-format msgid "" "%(failed)s notifications failed and %(success) succeeded. Failed ones are: " @@ -5379,7 +5602,7 @@ msgstr "" "%(failed)s értesítés sikertelen és %(success) sikeres. A sikertelenek: " "%(faileds)s." -#: vm/models/instance.py:668 +#: vm/models/instance.py:660 #, python-format msgid "" "%(failed)s notifications failed and %(success) succeeded. Failed ones are: " @@ -5388,49 +5611,49 @@ msgstr "" "%(failed)s értesítés sikertelen és %(success) sikeres. A sikertelenek: " "%(faileds_ex)s." -#: vm/models/instance.py:676 +#: vm/models/instance.py:668 #, python-format msgid "%(success)s notifications succeeded." msgstr "%(success)s sikeres értesítés." -#: vm/models/instance.py:681 +#: vm/models/instance.py:673 msgid "notify owner about expiration" msgstr "tulaj értesítése a lejáratról" -#: vm/models/instance.py:689 +#: vm/models/instance.py:681 #, python-format msgid "%(instance)s expiring soon" msgstr "%(instance)s hamarosan lejár" -#: vm/models/network.py:41 +#: vm/models/network.py:40 msgid "Network the interface belongs to." msgstr "Az a hálózat, amelyhez a példány tartozik." -#: vm/models/network.py:43 +#: vm/models/network.py:42 msgid "If a firewall host (i.e. IP address association) should be generated." msgstr "Tűzfal host generálása (IP cím hozzárendelése)." -#: vm/models/network.py:47 +#: vm/models/network.py:46 msgid "Template the interface template belongs to." msgstr "Sablon, amelyhez az interfészsablon tartozik." -#: vm/models/network.py:54 +#: vm/models/network.py:53 msgid "interface template" msgstr "interfészsablon" -#: vm/models/network.py:55 +#: vm/models/network.py:54 msgid "interface templates" msgstr "interfészsablonok" -#: vm/models/network.py:125 vm/models/network.py:130 +#: vm/models/network.py:124 vm/models/network.py:129 msgid "allocate IP address" msgstr "IP cím foglalása" -#: vm/models/network.py:136 +#: vm/models/network.py:135 msgid "Interface successfully created." msgstr "Az interfész létrehozásra került." -#: vm/models/network.py:137 +#: vm/models/network.py:136 #, python-format msgid "" "Interface successfully created. New addresses: ipv4: %(ip4)s, ipv6: %(ip6)s, " @@ -5503,60 +5726,10 @@ msgstr "passzív" msgid "active" msgstr "aktív" -#: vm/tasks/local_agent_tasks.py:40 -msgid "cleanup" -msgstr "takarítás" - -#: vm/tasks/local_agent_tasks.py:43 -msgid "change password" -msgstr "jelszóváltoztatás" - -#: vm/tasks/local_agent_tasks.py:45 -msgid "set time" -msgstr "óra beállítása" - -#: vm/tasks/local_agent_tasks.py:48 -msgid "set hostname" -msgstr "gépnév beállítása" - -#: vm/tasks/local_agent_tasks.py:56 -msgid "change ip" -msgstr "IP cím beállítása" - -#: vm/tasks/local_agent_tasks.py:60 -msgid "restart networking" -msgstr "hálózat újratöltése" - -#: vm/tasks/local_agent_tasks.py:93 -msgid "agent" -msgstr "ügynök" - -#: vm/tasks/local_agent_tasks.py:97 -msgid "starting" -msgstr "indítás" - -#: vm/tasks/local_agent_tasks.py:113 -msgid "wait agent restarting" -msgstr "várakozás az ügynök újraindulására" - -#: vm/tasks/local_agent_tasks.py:123 -msgid "start access server" -msgstr "távoli elérés indítása" - -#: vm/tasks/local_agent_tasks.py:154 +#: vm/tasks/local_agent_tasks.py:39 msgid "stopping" msgstr "leállítás" -#: vm/tasks/local_agent_tasks.py:170 -#, python-format -msgid "update to %(version)s" -msgstr "frissítés erre: %(version)s" - -#: vm/tasks/local_agent_tasks.py:177 -#, python-format -msgid "update agent to %(version)s" -msgstr "ügynökfrissítés erre: %(version)s" - #: vm/tasks/local_periodic_tasks.py:51 #, python-format msgid "%(instance)s destroyed" @@ -5585,19 +5758,28 @@ msgstr "" "%(instance)s gépe felfüggesztésre került, mivel " "lejárt. Felébresztheti vagy megsemmisítheti." -#: vm/tests/test_models.py:215 +#: vm/tests/test_models.py:218 msgid "x" msgstr "x" +#~ msgid "Visit" +#~ msgstr "Megtekintés" + +#~ msgid "change password" +#~ msgstr "jelszóváltoztatás" + +#~ msgid "start access server" +#~ msgstr "távoli elérés indítása" + +#~ msgid "update to %(version)s" +#~ msgstr "frissítés erre: %(version)s" + #~ msgid "Change the name of the node." #~ msgstr "Válasszon nevet a csomópontnak." #~ msgid "Flush" #~ msgstr "Ürítés" -#~ msgid "Disable node and move all instances to other one." -#~ msgstr "Csomópont letiltása és az összes példány migrálása a többire." - #~ msgid "Enable" #~ msgstr "Engedélyezés" diff --git a/circle/locale/hu/LC_MESSAGES/djangojs.po b/circle/locale/hu/LC_MESSAGES/djangojs.po index d441eb8..8add889 100644 --- a/circle/locale/hu/LC_MESSAGES/djangojs.po +++ b/circle/locale/hu/LC_MESSAGES/djangojs.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-09-24 12:19+0200\n" -"PO-Revision-Date: 2014-09-03 12:51+0200\n" +"POT-Creation-Date: 2014-10-20 12:09+0200\n" +"PO-Revision-Date: 2014-10-20 12:21+0200\n" "Last-Translator: Mate Ory \n" "Language-Team: Hungarian \n" "Language: en_US\n" @@ -88,9 +88,9 @@ msgstr "Válasszon a folytatáshoz." #: 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 +#: static_collected/dashboard/dashboard.js:259 +#: static_collected/dashboard/dashboard.js:307 +#: static_collected/dashboard/dashboard.js:317 msgid "No result" msgstr "Nincs eredmény" @@ -128,6 +128,14 @@ msgstr "Nincs jogosultsága a profil módosításához." msgid "Unknown error." msgstr "Ismeretlen hiba." +#: dashboard/static/dashboard/template-list.js:103 +msgid "Only the owners can delete the selected object." +msgstr "Csak a tulajdonos törölheti a kiválasztott elemet." + +#: dashboard/static/dashboard/template-list.js:105 +msgid "An error occurred. (" +msgstr "Hiba történt. (" + #: dashboard/static/dashboard/vm-create.js:111 #: dashboard/static/dashboard/vm-create.js:174 #: static_collected/all.047675ebf594.js:4813 @@ -176,33 +184,17 @@ msgstr "Nincs több hálózat." 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 +#: dashboard/static/dashboard/vm-details.js:116 +#: static_collected/dashboard/vm-details.js:116 msgid "Hide password" msgstr "Jelszó rejtése" -#: dashboard/static/dashboard/vm-details.js:119 -#: static_collected/dashboard/vm-details.js:119 +#: dashboard/static/dashboard/vm-details.js:120 +#: static_collected/dashboard/vm-details.js:120 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 +#: dashboard/static/dashboard/vm-tour.js:6 #: static_collected/vm-detail.09737c69abc3.js:5855 #: static_collected/vm-detail.15d710d8ccf0.js:6391 #: static_collected/vm-detail.234990ca6ec1.js:6964 @@ -218,7 +210,11 @@ msgstr "Vissza" msgid "Next" msgstr "Tovább" -#: dashboard/static/dashboard/vm-tour.js:26 +#: dashboard/static/dashboard/vm-tour.js:7 +msgid "Previous" +msgstr "Vissza" + +#: dashboard/static/dashboard/vm-tour.js:8 #: static_collected/vm-detail.09737c69abc3.js:5859 #: static_collected/vm-detail.15d710d8ccf0.js:6395 #: static_collected/vm-detail.234990ca6ec1.js:6968 @@ -234,43 +230,19 @@ msgstr "Tovább" 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:9 +msgid "Done" +msgstr "Kész" -#: 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 +#: dashboard/static/dashboard/vm-tour.js:56 msgid "" -"Welcome to the template tutorial. In this quick tour, we gonna show you how " -"to do the steps described above." +"Welcome to the template tutorial. In this quick tour, we are going to show " +"you how to do the steps described above." msgstr "" "Üdvözöli a sablon-kalauz. A túra során bemutatjuk, hogyan végezze el a fenti " "lépéseket." -#: dashboard/static/dashboard/vm-tour.js:35 +#: dashboard/static/dashboard/vm-tour.js:57 #: static_collected/vm-detail.09737c69abc3.js:5868 #: static_collected/vm-detail.15d710d8ccf0.js:6404 #: static_collected/vm-detail.234990ca6ec1.js:6977 @@ -290,111 +262,41 @@ msgstr "" "A következő lépéshez kattintson a \"Tovább\" gombra vagy használja a " "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 +#: dashboard/static/dashboard/vm-tour.js:61 msgid "" -"During the tour please don't try the functions because it may lead to " -"graphical glitches, however " -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" +"In this tab you can extend the expiration date of your virtual machine, add " +"tags and modify the name and description." +msgstr "" +"Ezen a lapon megújíthatja a virtuális gép lejáratát, címkéket adhat hozzá, " +"vagy módosíthatja a nevét, leírását." -#: 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 +#: dashboard/static/dashboard/vm-tour.js:65 msgid "" -"In this tab you can tag your virtual machine and modify the name and " -"description." +"Please add a meaningful description to the virtual machine. Changing the " +"name is also recommended, however you can choose a new name when saving the " +"template." msgstr "" -"Ezen a lapon címkéket adhat a virtuális géphez, vagy módosíthatja a nevét, " -"leírását." +"Kérjük, adjon meg egy informatív leírást. A név megváltoztatása is " +"ajánlott, azonban a mentéskor is van a sablon nevének " +"megválasztására." -#: 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:69 +msgid "" +"You can change the lease to extend the expiration date. This will be the " +"lease of the new template." +msgstr "" +"Megváltoztathatja a bérleti módot is a lejárat bővítéséhez. Az gép " +"bérleti módját örökli majd a sablon is." -#: 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 +#: dashboard/static/dashboard/vm-tour.js:73 msgid "" -"On the resources tab you can edit the CPU/RAM options and add/remove disks!" +"On the resources tab you can edit the CPU/RAM options and add/remove disks " +"if you have required permissions." msgstr "" "Az erőforrások lapon szerkesztheti a CPU/memória-beállításokat, valamint " -"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" +"hozzáadhat és törölhet lemezeket, ha van ehhez jogosultsága." -#: dashboard/static/dashboard/vm-tour.js:69 +#: dashboard/static/dashboard/vm-tour.js:81 #: static_collected/vm-detail.09737c69abc3.js:5902 #: static_collected/vm-detail.15d710d8ccf0.js:6438 #: static_collected/vm-detail.234990ca6ec1.js:7011 @@ -410,7 +312,7 @@ msgstr "Erőforrások" msgid "CPU priority" msgstr "CPU prioritás" -#: dashboard/static/dashboard/vm-tour.js:69 +#: dashboard/static/dashboard/vm-tour.js:82 #: static_collected/vm-detail.09737c69abc3.js:5902 #: static_collected/vm-detail.15d710d8ccf0.js:6438 #: static_collected/vm-detail.234990ca6ec1.js:7011 @@ -426,7 +328,7 @@ msgstr "CPU prioritás" msgid "higher is better" msgstr "a nagyobb érték a jobb" -#: dashboard/static/dashboard/vm-tour.js:70 +#: dashboard/static/dashboard/vm-tour.js:83 #: static_collected/vm-detail.09737c69abc3.js:5903 #: static_collected/vm-detail.15d710d8ccf0.js:6439 #: static_collected/vm-detail.234990ca6ec1.js:7012 @@ -442,7 +344,7 @@ msgstr "a nagyobb érték a jobb" msgid "CPU count" msgstr "CPU-k száma" -#: dashboard/static/dashboard/vm-tour.js:70 +#: dashboard/static/dashboard/vm-tour.js:84 #: static_collected/vm-detail.09737c69abc3.js:5903 #: static_collected/vm-detail.15d710d8ccf0.js:6439 #: static_collected/vm-detail.234990ca6ec1.js:7012 @@ -458,7 +360,7 @@ msgstr "CPU-k száma" msgid "number of CPU cores." msgstr "A CPU-magok száma." -#: dashboard/static/dashboard/vm-tour.js:71 +#: dashboard/static/dashboard/vm-tour.js:85 #: static_collected/vm-detail.09737c69abc3.js:5904 #: static_collected/vm-detail.15d710d8ccf0.js:6440 #: static_collected/vm-detail.234990ca6ec1.js:7013 @@ -474,7 +376,7 @@ msgstr "A CPU-magok száma." msgid "RAM amount" msgstr "RAM mennyiség" -#: dashboard/static/dashboard/vm-tour.js:71 +#: dashboard/static/dashboard/vm-tour.js:86 #: static_collected/vm-detail.09737c69abc3.js:5904 #: static_collected/vm-detail.15d710d8ccf0.js:6440 #: static_collected/vm-detail.234990ca6ec1.js:7013 @@ -490,23 +392,7 @@ msgstr "RAM mennyiség" 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 +#: dashboard/static/dashboard/vm-tour.js:96 #: static_collected/vm-detail.09737c69abc3.js:5915 #: static_collected/vm-detail.15d710d8ccf0.js:6451 #: static_collected/vm-detail.234990ca6ec1.js:7024 @@ -525,23 +411,7 @@ msgstr "" "Hozzáadhat üres lemezeket, letölthet lemezképeket, vagy törölheti a " "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 +#: dashboard/static/dashboard/vm-tour.js:105 #: static_collected/vm-detail.09737c69abc3.js:5926 #: static_collected/vm-detail.15d710d8ccf0.js:6462 #: static_collected/vm-detail.234990ca6ec1.js:7035 @@ -557,23 +427,7 @@ msgstr "Hálózat lap" 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 +#: dashboard/static/dashboard/vm-tour.js:109 #: static_collected/vm-detail.09737c69abc3.js:5938 #: static_collected/vm-detail.15d710d8ccf0.js:6474 #: static_collected/vm-detail.234990ca6ec1.js:7047 @@ -589,55 +443,15 @@ msgstr "Indítás" 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" +msgid "" +"Use the CIRCLE client or the connection string to connect to the virtual " +"machine." +msgstr "" +"Használja a CIRCLE klienst vagy a kapcsolódási adatokat a " +"virtuális géphez való csatlakozáshoz." -#: dashboard/static/dashboard/vm-tour.js:121 +#: dashboard/static/dashboard/vm-tour.js:117 #: static_collected/vm-detail.09737c69abc3.js:5954 #: static_collected/vm-detail.15d710d8ccf0.js:6490 #: static_collected/vm-detail.234990ca6ec1.js:7063 @@ -657,23 +471,7 @@ msgstr "" "Miután csatlakozott, végezze el a szükséges módosításokat, majd jelentkezzen " "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 +#: dashboard/static/dashboard/vm-tour.js:121 #: static_collected/vm-detail.09737c69abc3.js:5962 #: static_collected/vm-detail.15d710d8ccf0.js:6498 #: static_collected/vm-detail.234990ca6ec1.js:7071 @@ -692,39 +490,13 @@ msgstr "" "Kattintson a „mentés sablonként” gombra, majd várjon, amíg a lemez mentése " "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 +#: dashboard/static/dashboard/vm-tour.js:125 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." +"again." +msgstr "" +"A túra véget ért. Ha valami nem érthető, újrakezdheti az " +"útmutatót." #: network/static/js/host.js:10 static_collected/all.047675ebf594.js:5239 #: static_collected/all.0aecd87e873a.js:5309 @@ -834,6 +606,290 @@ msgstr "Eltávolítás" msgid "Are you sure you want to delete this device?" msgstr "Biztosan törli ezt az eszközt?" +#: 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" + +#: 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" + +#: 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 " +"to do the steps described above." +msgstr "" +"Üdvözöli a sablon-kalauz. A túra során bemutatjuk, hogyan végezze el a fenti " +"lépéseket." + +#: 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 " +"graphical glitches, however " +msgstr "A túra során még ne próbálja ki a bemutatott funkciókat." + +#: 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" + +#: 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 " +"description." +msgstr "" +"Ezen a lapon címkéket adhat a virtuális géphez, vagy módosíthatja a nevét, " +"leírását." + +#: 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" + +#: 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!" +msgstr "" +"Az erőforrások lapon szerkesztheti a CPU/memória-beállításokat, valamint " +"hozzáadhat és törölhet lemezeket." + +#: 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" + +#: 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" + +#: 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" + +#: 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" + +#: 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" + +#: 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." + +#: 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" + +#: 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" + +#: 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" + +#: 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." + #: static_collected/vm-detail.e81fe84bf4c0.js:9 msgid "" "During the tour please don't try the functions because it may lead to " diff --git a/circle/manager/mancelery.py b/circle/manager/mancelery.py index 3268fd9..ab095fb 100755 --- a/circle/manager/mancelery.py +++ b/circle/manager/mancelery.py @@ -16,12 +16,15 @@ # with CIRCLE. If not, see . from celery import Celery +from celery.signals import worker_ready 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/") +QUEUE_NAME = HOSTNAME + '.man' + celery = Celery('manager', broker=getenv("AMQP_URI"), @@ -57,3 +60,10 @@ celery.conf.update( } ) + + +@worker_ready.connect() +def cleanup_tasks(conf=None, **kwargs): + '''Discard all task and clean up activity.''' + from vm.models.activity import cleanup + cleanup(queue_name=QUEUE_NAME) diff --git a/circle/manager/moncelery.py b/circle/manager/moncelery.py index 314ec2e..1ff01c1 100755 --- a/circle/manager/moncelery.py +++ b/circle/manager/moncelery.py @@ -16,12 +16,14 @@ # with CIRCLE. If not, see . from celery import Celery +from celery.signals import worker_ready 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/") +QUEUE_NAME = HOSTNAME + '.monitor' celery = Celery('monitor', broker=getenv("AMQP_URI"), @@ -34,7 +36,7 @@ celery.conf.update( CELERY_CACHE_BACKEND=CACHE_URI, CELERY_TASK_RESULT_EXPIRES=300, CELERY_QUEUES=( - Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'), + Queue(QUEUE_NAME, Exchange('monitor', type='direct'), routing_key="monitor"), ), CELERYBEAT_SCHEDULE={ @@ -70,3 +72,10 @@ celery.conf.update( } ) + + +@worker_ready.connect() +def cleanup_tasks(conf=None, **kwargs): + '''Discard all task and clean up activity.''' + from vm.models.activity import cleanup + cleanup(queue_name=QUEUE_NAME) diff --git a/circle/manager/slowcelery.py b/circle/manager/slowcelery.py index ee8eba1..c06d7d5 100755 --- a/circle/manager/slowcelery.py +++ b/circle/manager/slowcelery.py @@ -16,12 +16,14 @@ # with CIRCLE. If not, see . from celery import Celery +from celery.signals import worker_ready 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/") +QUEUE_NAME = HOSTNAME + '.man.slow' celery = Celery('manager.slow', broker=getenv("AMQP_URI"), @@ -36,7 +38,7 @@ celery.conf.update( CELERY_CACHE_BACKEND=CACHE_URI, CELERY_TASK_RESULT_EXPIRES=300, CELERY_QUEUES=( - Queue(HOSTNAME + '.man.slow', Exchange('manager.slow', type='direct'), + Queue(QUEUE_NAME, Exchange('manager.slow', type='direct'), routing_key="manager.slow"), ), CELERYBEAT_SCHEDULE={ @@ -48,3 +50,10 @@ celery.conf.update( } ) + + +@worker_ready.connect() +def cleanup_tasks(conf=None, **kwargs): + '''Discard all task and clean up activity.''' + from vm.models.activity import cleanup + cleanup(queue_name=QUEUE_NAME) diff --git a/circle/network/views.py b/circle/network/views.py index 5a42836..26f6d76 100644 --- a/circle/network/views.py +++ b/circle/network/views.py @@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, context = super(VlanDetail, self).get_context_data(**kwargs) q = Host.objects.filter(interface__in=Interface.objects.filter( - vlan=self.object, instance__destroyed_at=None + vlan=self.object )) context['host_list'] = SmallHostTable(q) diff --git a/circle/storage/models.py b/circle/storage/models.py index 9a911e4..d1a4b1d 100644 --- a/circle/storage/models.py +++ b/circle/storage/models.py @@ -416,6 +416,7 @@ class Disk(TimeStampedModel): "Operation aborted by user."), e) disk.size = result['size'] disk.type = result['type'] + disk.checksum = result.get('checksum', None) disk.is_ready = True disk.save() return disk @@ -490,6 +491,9 @@ class Disk(TimeStampedModel): disk.destroy() raise humanize_exception(ugettext_noop( "Operation aborted by user."), e) + except: + disk.destroy() + raise disk.is_ready = True disk.save() return disk diff --git a/circle/vm/models/__init__.py b/circle/vm/models/__init__.py index 26f5c49..14722fb 100644 --- a/circle/vm/models/__init__.py +++ b/circle/vm/models/__init__.py @@ -1,13 +1,11 @@ # flake8: noqa from .activity import InstanceActivity -from .activity import instance_activity from .activity import NodeActivity from .activity import node_activity from .common import BaseResourceConfigModel from .common import Lease from .common import NamedBaseResourceConfig from .common import Trait -from .instance import InstanceActiveManager from .instance import VirtualMachineDescModel from .instance import InstanceTemplate from .instance import Instance @@ -19,9 +17,9 @@ from .network import Interface from .node import Node __all__ = [ - 'InstanceActivity', 'InstanceActiveManager', 'BaseResourceConfigModel', + 'InstanceActivity', 'BaseResourceConfigModel', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', - 'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed', - 'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', - 'node_activity', 'pwgen' + 'Instance', 'post_state_changed', 'pre_state_changed', 'InterfaceTemplate', + 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'node_activity', + 'pwgen' ] diff --git a/circle/vm/models/activity.py b/circle/vm/models/activity.py index e2b75ca..ad7736b 100644 --- a/circle/vm/models/activity.py +++ b/circle/vm/models/activity.py @@ -20,7 +20,6 @@ from contextlib import contextmanager from logging import getLogger from warnings import warn -from celery.signals import worker_ready from celery.contrib.abortable import AbortableAsyncResult from django.core.urlresolvers import reverse @@ -206,21 +205,6 @@ class InstanceActivity(ActivityModel): 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, 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, - resultant_state=resultant_state) - return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit) - - class NodeActivity(ActivityModel): ACTIVITY_CODE_BASE = join_activity_code('vm', 'Node') node = ForeignKey('Node', related_name='activity_log', @@ -278,17 +262,17 @@ def node_activity(code_suffix, node, task_uuid=None, user=None, return activitycontextimpl(act) -@worker_ready.connect() def cleanup(conf=None, **kwargs): # TODO check if other manager workers are running - from celery.task.control import discard_all - discard_all() msg_txt = ugettext_noop("Manager is restarted, activity is cleaned up. " "You can try again now.") message = create_readable(msg_txt, msg_txt) + queue_name = kwargs.get('queue_name', None) for i in InstanceActivity.objects.filter(finished__isnull=True): - i.finish(False, result=message) - logger.error('Forced finishing stale activity %s', i) + op = i.get_operation() + if op and op.async_queue == queue_name: + i.finish(False, result=message) + logger.error('Forced finishing stale activity %s', i) for i in NodeActivity.objects.filter(finished__isnull=True): i.finish(False, result=message) logger.error('Forced finishing stale activity %s', i) diff --git a/circle/vm/models/instance.py b/circle/vm/models/instance.py index bad9d54..541b7c1 100644 --- a/circle/vm/models/instance.py +++ b/circle/vm/models/instance.py @@ -16,15 +16,13 @@ # with CIRCLE. If not, see . from __future__ import absolute_import, unicode_literals +from contextlib import contextmanager from datetime import timedelta from functools import partial from importlib import import_module from logging import getLogger -from string import ascii_lowercase from warnings import warn -from celery.exceptions import TimeoutError -from celery.contrib.abortable import AbortableAsyncResult import django.conf from django.contrib.auth.models import User from django.core import signing @@ -38,17 +36,17 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ugettext_noop from model_utils import Choices +from model_utils.managers import QueryManager from model_utils.models import TimeStampedModel, StatusModel from taggit.managers import TaggableManager from acl.models import AclBase from common.models import ( - create_readable, HumanReadableException, humanize_exception + activitycontextimpl, create_readable, HumanReadableException, ) from common.operations import OperatedMixin -from ..tasks import vm_tasks, agent_tasks -from .activity import (ActivityInProgressError, instance_activity, - InstanceActivity) +from ..tasks import agent_tasks +from .activity import (ActivityInProgressError, InstanceActivity) from .common import BaseResourceConfigModel, Lease from .network import Interface from .node import Node, Trait @@ -92,13 +90,6 @@ def find_unused_vnc_port(): return port -class InstanceActiveManager(Manager): - - def get_query_set(self): - return super(InstanceActiveManager, - self).get_query_set().filter(destroyed_at=None) - - class VirtualMachineDescModel(BaseResourceConfigModel): """Abstract base for virtual machine describing models. @@ -264,7 +255,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, help_text=_("The virtual machine's time of " "destruction.")) objects = Manager() - active = InstanceActiveManager() + active = QueryManager(destroyed_at=None) class Meta: app_label = 'vm' @@ -374,9 +365,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, def __on_commit(activity): activity.resultant_state = 'PENDING' - with instance_activity(code_suffix='create', instance=inst, - readable_name=ugettext_noop("create instance"), - on_commit=__on_commit, user=inst.owner) as act: + with inst.activity(code_suffix='create', + readable_name=ugettext_noop("create instance"), + on_commit=__on_commit, user=inst.owner) as act: # create related entities inst.disks.add(*[disk.get_exclusive() for disk in disks]) @@ -677,10 +668,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, "%(success)s notifications succeeded."), success=len(success), successes=success) - with instance_activity('notification_about_expiration', instance=self, - readable_name=ugettext_noop( - "notify owner about expiration"), - on_commit=on_commit): + with self.activity('notification_about_expiration', + readable_name=ugettext_noop( + "notify owner about expiration"), + on_commit=on_commit): from dashboard.views import VmRenewView, absolute_url level = self.get_level_object("owner") for u, ulevel in self.get_users_with_level(level__pk=level.pk): @@ -745,75 +736,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, """ return scheduler.select_node(self, Node.objects.all()) - def attach_disk(self, disk, timeout=15): - queue_name = self.get_remote_queue_name('vm', 'fast') - return vm_tasks.attach_disk.apply_async( - args=[self.vm_name, - disk.get_vmdisk_desc()], - queue=queue_name - ).get(timeout=timeout) - - def detach_disk(self, disk, timeout=15): - try: - queue_name = self.get_remote_queue_name('vm', 'fast') - return vm_tasks.detach_disk.apply_async( - args=[self.vm_name, - disk.get_vmdisk_desc()], - queue=queue_name - ).get(timeout=timeout) - except Exception as e: - if e.libvirtError and "not found" in str(e): - logger.debug("Disk %s was not found." - % disk.name) - else: - raise - - def attach_network(self, network, timeout=15): - queue_name = self.get_remote_queue_name('vm', 'fast') - return vm_tasks.attach_network.apply_async( - args=[self.vm_name, - network.get_vmnetwork_desc()], - queue=queue_name - ).get(timeout=timeout) - - def detach_network(self, network, timeout=15): - try: - queue_name = self.get_remote_queue_name('vm', 'fast') - return vm_tasks.detach_network.apply_async( - args=[self.vm_name, - network.get_vmnetwork_desc()], - queue=queue_name - ).get(timeout=timeout) - except Exception as e: - if e.libvirtError and "not found" in str(e): - logger.debug("Interface %s was not found." - % (network.__unicode__())) - else: - raise - - def resize_disk_live(self, disk, size, timeout=15): - queue_name = self.get_remote_queue_name('vm', 'slow') - result = vm_tasks.resize_disk.apply_async( - args=[self.vm_name, disk.path, size], - queue=queue_name).get(timeout=timeout) - disk.size = size - disk.save() - return result - - def deploy_disks(self): - """Deploy all associated disks. - """ - devnums = list(ascii_lowercase) # a-z - for disk in self.disks.all(): - # assign device numbers - if disk.dev_num in devnums: - devnums.remove(disk.dev_num) - else: - disk.dev_num = devnums.pop(0) - disk.save() - # deploy disk - disk.deploy() - def destroy_disks(self): """Destroy all associated disks. """ @@ -838,96 +760,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, for net in self.interface_set.all(): net.shutdown() - def delete_vm(self, timeout=15): - queue_name = self.get_remote_queue_name('vm', 'fast') - try: - return vm_tasks.destroy.apply_async(args=[self.vm_name], - queue=queue_name - ).get(timeout=timeout) - except Exception as e: - if e.libvirtError and "Domain not found" in str(e): - logger.debug("Domain %s was not found at %s" - % (self.vm_name, queue_name)) - else: - raise - - def deploy_vm(self, timeout=15): - queue_name = self.get_remote_queue_name('vm', 'slow') - return vm_tasks.deploy.apply_async(args=[self.get_vm_desc()], - queue=queue_name - ).get(timeout=timeout) - - def migrate_vm(self, to_node, timeout=120): - queue_name = self.get_remote_queue_name('vm', 'slow') - return vm_tasks.migrate.apply_async(args=[self.vm_name, - to_node.host.hostname, - True], - queue=queue_name - ).get(timeout=timeout) - - def reboot_vm(self, timeout=5): - queue_name = self.get_remote_queue_name('vm', 'fast') - return vm_tasks.reboot.apply_async(args=[self.vm_name], - queue=queue_name - ).get(timeout=timeout) - - def reset_vm(self, timeout=5): - queue_name = self.get_remote_queue_name('vm', 'fast') - return vm_tasks.reset.apply_async(args=[self.vm_name], - queue=queue_name - ).get(timeout=timeout) - - def resume_vm(self, timeout=15): - queue_name = self.get_remote_queue_name('vm', 'slow') - return vm_tasks.resume.apply_async(args=[self.vm_name], - queue=queue_name - ).get(timeout=timeout) - - def shutdown_vm(self, task=None, step=5): - queue_name = self.get_remote_queue_name('vm', 'slow') - logger.debug("RPC Shutdown at queue: %s, for vm: %s.", queue_name, - self.vm_name) - remote = vm_tasks.shutdown.apply_async(kwargs={'name': self.vm_name}, - queue=queue_name) - - while True: - try: - return remote.get(timeout=step) - except TimeoutError as e: - if task is not None and task.is_aborted(): - AbortableAsyncResult(remote.id).abort() - raise humanize_exception(ugettext_noop( - "Operation aborted by user."), e) - - def suspend_vm(self, timeout=230): - queue_name = self.get_remote_queue_name('vm', 'slow') - return vm_tasks.sleep.apply_async(args=[self.vm_name, - self.mem_dump['path']], - queue=queue_name - ).get(timeout=timeout) - - def wake_up_vm(self, timeout=60): - queue_name = self.get_remote_queue_name('vm', 'slow') - return vm_tasks.wake_up.apply_async(args=[self.vm_name, - self.mem_dump['path']], - queue=queue_name - ).get(timeout=timeout) - - def delete_mem_dump(self, timeout=15): - queue_name = self.mem_dump['datastore'].get_remote_queue_name( - 'storage', 'fast') - from storage.tasks.storage_tasks import delete_dump - delete_dump.apply_async(args=[self.mem_dump['path']], - queue=queue_name).get(timeout=timeout) - - def allocate_node(self, activity): - if self.node is not None: - return None - - with activity.sub_activity( - 'scheduling', - readable_name=ugettext_noop("schedule")) as sa: - sa.result = self.node = self.select_node() + def allocate_node(self): + if self.node is None: + self.node = self.select_node() self.save() return self.node @@ -1002,12 +837,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, return merged_acts - def get_screenshot(self, timeout=5): - queue_name = self.get_remote_queue_name("vm", "fast") - return vm_tasks.screenshot.apply_async(args=[self.vm_name], - queue=queue_name - ).get(timeout=timeout) - def get_latest_activity_in_progress(self): try: return InstanceActivity.objects.filter( @@ -1023,3 +852,17 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, @property def metric_prefix(self): return 'vm.%s' % self.vm_name + + @contextmanager + def activity(self, code_suffix, readable_name, on_abort=None, + on_commit=None, task_uuid=None, user=None, + concurrency_check=True, 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=code_suffix, instance=self, task_uuid=task_uuid, + user=user, concurrency_check=concurrency_check, + readable_name=readable_name, resultant_state=resultant_state) + return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit) diff --git a/circle/vm/models/network.py b/circle/vm/models/network.py index c7c3fb2..696841b 100644 --- a/circle/vm/models/network.py +++ b/circle/vm/models/network.py @@ -26,7 +26,6 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop from common.models import create_readable from firewall.models import Vlan, Host from ..tasks import net_tasks -from .activity import instance_activity logger = getLogger(__name__) @@ -120,10 +119,10 @@ class Interface(Model): host.hostname = instance.vm_name # Get addresses from firewall if base_activity is None: - act_ctx = instance_activity( + act_ctx = instance.activity( code_suffix='allocating_ip', readable_name=ugettext_noop("allocate IP address"), - instance=instance, user=owner) + user=owner) else: act_ctx = base_activity.sub_activity( 'allocating_ip', diff --git a/circle/vm/models/node.py b/circle/vm/models/node.py index 2c52816..0684d13 100644 --- a/circle/vm/models/node.py +++ b/circle/vm/models/node.py @@ -88,7 +88,9 @@ class Node(OperatedMixin, TimeStampedModel): class Meta: app_label = 'vm' db_table = 'vm_node' - permissions = () + permissions = ( + ('view_statistics', _('Can view Node box and statistics.')), + ) ordering = ('-enabled', 'normalized_name') def __unicode__(self): @@ -114,8 +116,8 @@ class Node(OperatedMixin, TimeStampedModel): def get_info(self): return self.remote_query(vm_tasks.get_info, priority='fast', - default={'core_num': '', - 'ram_size': '0', + default={'core_num': 0, + 'ram_size': 0, 'architecture': ''}) info = property(get_info) @@ -313,10 +315,11 @@ class Node(OperatedMixin, TimeStampedModel): def get_status_label(self): return { 'OFFLINE': 'label-warning', - 'DISABLED': 'label-warning', + 'DISABLED': 'label-danger', 'MISSING': 'label-danger', - 'ONLINE': 'label-success'}.get(self.get_state(), - 'label-danger') + 'ACTIVE': 'label-success', + 'PASSIVE': 'label-warning', + }.get(self.get_state(), 'label-danger') @node_available def update_vm_states(self): diff --git a/circle/vm/operations.py b/circle/vm/operations.py index 53d5e8c..f790b6b 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -16,24 +16,32 @@ # with CIRCLE. If not, see . from __future__ import absolute_import, unicode_literals +from base64 import encodestring +from hashlib import md5 from logging import getLogger +import os from re import search from string import ascii_lowercase +from StringIO import StringIO +from tarfile import TarFile, TarInfo +import time 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 django.db.models import Q from sizefield.utils import filesizeformat -from celery.exceptions import TimeLimitExceeded +from celery.contrib.abortable import AbortableAsyncResult +from celery.exceptions import TimeLimitExceeded, TimeoutError from common.models import ( create_readable, humanize_exception, HumanReadableException ) -from common.operations import Operation, register_operation +from common.operations import Operation, register_operation, SubOperationMixin from manager.scheduler import SchedulerError from .tasks.local_tasks import ( abortable_async_instance_operation, abortable_async_node_operation, @@ -42,13 +50,48 @@ from .models import ( Instance, InstanceActivity, InstanceTemplate, Interface, Node, NodeActivity, pwgen ) -from .tasks import agent_tasks, local_agent_tasks +from .tasks import agent_tasks, vm_tasks from dashboard.store_api import Store, NoStoreException +from firewall.models import Host +from monitor.client import Client +from storage.tasks import storage_tasks logger = getLogger(__name__) +class RemoteOperationMixin(object): + + remote_timeout = 30 + + def _operation(self, **kwargs): + args = self._get_remote_args(**kwargs) + return self.task.apply_async( + args=args, queue=self._get_remote_queue() + ).get(timeout=self.remote_timeout) + + def check_precond(self): + super(RemoteOperationMixin, self).check_precond() + self._get_remote_queue() + + +class AbortableRemoteOperationMixin(object): + remote_step = property(lambda self: self.remote_timeout / 10) + + def _operation(self, task, **kwargs): + args = self._get_remote_args(**kwargs), + remote = self.task.apply_async( + args=args, queue=self._get_remote_queue()) + for i in xrange(0, self.remote_timeout, self.remote_step): + try: + return remote.get(timeout=self.remote_step) + except TimeoutError as e: + if task is not None and task.is_aborted(): + AbortableAsyncResult(remote.id).abort() + raise humanize_exception(ugettext_noop( + "Operation aborted by user."), e) + + class InstanceOperation(Operation): acl_level = 'owner' async_operation = abortable_async_instance_operation @@ -100,12 +143,13 @@ class InstanceOperation(Operation): "parent activity does not match the user " "provided as parameter.") - return parent.create_sub(code_suffix=self.activity_code_suffix, - readable_name=name, - resultant_state=self.resultant_state) + return parent.create_sub( + code_suffix=self.get_activity_code_suffix(), + readable_name=name, resultant_state=self.resultant_state) else: return InstanceActivity.create( - code_suffix=self.activity_code_suffix, instance=self.instance, + code_suffix=self.get_activity_code_suffix(), + instance=self.instance, readable_name=name, user=user, concurrency_check=self.concurrency_check, resultant_state=self.resultant_state) @@ -116,9 +160,43 @@ class InstanceOperation(Operation): return False +class RemoteInstanceOperation(RemoteOperationMixin, InstanceOperation): + + remote_queue = ('vm', 'fast') + + def _get_remote_queue(self): + return self.instance.get_remote_queue_name(*self.remote_queue) + + def _get_remote_args(self, **kwargs): + return [self.instance.vm_name] + + +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 RemoteAgentOperation(EnsureAgentMixin, RemoteInstanceOperation): + remote_queue = ('agent', ) + concurrency_check = False + + @register_operation class AddInterfaceOperation(InstanceOperation): - activity_code_suffix = 'add_interface' id = 'add_interface' name = _("add interface") description = _("Add a new network interface for the specified VLAN to " @@ -146,16 +224,15 @@ class AddInterfaceOperation(InstanceOperation): if self.instance.is_running: try: - with activity.sub_activity( - 'attach_network', - readable_name=ugettext_noop("attach network")): - self.instance.attach_network(net) + self.instance._attach_network( + interface=net, parent_activity=activity) except Exception as e: if hasattr(e, 'libvirtError'): self.rollback(net, activity) raise net.deploy() - local_agent_tasks.send_networking_commands(self.instance, activity) + self.instance._change_ip(parent_activity=activity) + self.instance._restart_networking(parent_activity=activity) def get_activity_name(self, kwargs): return create_readable(ugettext_noop("add %(vlan)s interface"), @@ -165,7 +242,6 @@ class AddInterfaceOperation(InstanceOperation): @register_operation class CreateDiskOperation(InstanceOperation): - activity_code_suffix = 'create_disk' id = 'create_disk' name = _("create disk") description = _("Create and attach empty disk to the virtual machine.") @@ -192,11 +268,7 @@ class CreateDiskOperation(InstanceOperation): readable_name=ugettext_noop("deploying disk") ): disk.deploy() - with activity.sub_activity( - 'attach_disk', - readable_name=ugettext_noop("attach disk") - ): - self.instance.attach_disk(disk) + self.instance._attach_disk(parent_activity=activity, disk=disk) def get_activity_name(self, kwargs): return create_readable( @@ -205,9 +277,8 @@ class CreateDiskOperation(InstanceOperation): @register_operation -class ResizeDiskOperation(InstanceOperation): +class ResizeDiskOperation(RemoteInstanceOperation): - activity_code_suffix = 'resize_disk' id = 'resize_disk' name = _("resize disk") description = _("Resize the virtual disk image. " @@ -215,19 +286,26 @@ class ResizeDiskOperation(InstanceOperation): required_perms = ('storage.resize_disk', ) accept_states = ('RUNNING', ) async_queue = "localhost.man.slow" + remote_queue = ('vm', 'slow') + task = vm_tasks.resize_disk - def _operation(self, user, disk, size, activity): - self.instance.resize_disk_live(disk, size) + def _get_remote_args(self, disk, size, **kwargs): + return (super(ResizeDiskOperation, self) + ._get_remote_args(**kwargs) + [disk.path, size]) def get_activity_name(self, kwargs): return create_readable( ugettext_noop("resize disk %(name)s to %(size)s"), size=filesizeformat(kwargs['size']), name=kwargs['disk'].name) + def _operation(self, disk, size): + super(ResizeDiskOperation, self)._operation(disk=disk, size=size) + disk.size = size + disk.save() + @register_operation class DownloadDiskOperation(InstanceOperation): - activity_code_suffix = 'download_disk' id = 'download_disk' name = _("download disk") description = _("Download and attach disk image (ISO file) for the " @@ -241,7 +319,6 @@ class DownloadDiskOperation(InstanceOperation): async_queue = "localhost.man.slow" def _operation(self, user, url, task, activity, name=None): - activity.result = url from storage.models import Disk disk = Disk.download(url=url, name=name, task=task) @@ -255,18 +332,17 @@ class DownloadDiskOperation(InstanceOperation): activity.readable_name = create_readable( ugettext_noop("download %(name)s"), name=disk.name) + activity.result = create_readable(ugettext_noop( + "Downloading %(url)s is finished. The file md5sum " + "is: '%(checksum)s'."), + url=url, checksum=disk.checksum) # TODO iso (cd) hot-plug is not supported by kvm/guests if self.instance.is_running and disk.type not in ["iso"]: - with activity.sub_activity( - 'attach_disk', - readable_name=ugettext_noop("attach disk") - ): - self.instance.attach_disk(disk) + self.instance._attach_disk(parent_activity=activity, disk=disk) @register_operation class DeployOperation(InstanceOperation): - activity_code_suffix = 'deploy' id = 'deploy' name = _("deploy") description = _("Deploy and start the virtual machine (including storage " @@ -290,25 +366,21 @@ class DeployOperation(InstanceOperation): "deployed to node: %(node)s"), node=self.instance.node) - def _operation(self, activity, timeout=15): + def _operation(self, activity, node=None): # Allocate VNC port and host node self.instance.allocate_vnc_port() - self.instance.allocate_node(activity) + if node is not None: + self.instance.node = node + self.instance.save() + else: + self.instance.allocate_node() # Deploy virtual images - with activity.sub_activity( - 'deploying_disks', readable_name=ugettext_noop( - "deploy disks")): - self.instance.deploy_disks() + self.instance._deploy_disks(parent_activity=activity) # 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=rn) as deploy_act: - deploy_act.result = self.instance.deploy_vm(timeout=timeout) + self.instance._deploy_vm(parent_activity=activity) # Establish network connection (vmdriver) with activity.sub_activity( @@ -321,20 +393,57 @@ class DeployOperation(InstanceOperation): except: pass - # Resume vm - with activity.sub_activity( - 'booting', readable_name=ugettext_noop( - "boot virtual machine")): - self.instance.resume_vm(timeout=timeout) + self.instance._resume_vm(parent_activity=activity) if self.instance.has_agent: activity.sub_activity('os_boot', readable_name=ugettext_noop( "wait operating system loading"), interruptible=True) + @register_operation + class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation): + id = "_deploy_vm" + name = _("deploy vm") + description = _("Deploy virtual machine.") + remote_queue = ("vm", "slow") + task = vm_tasks.deploy + + def _get_remote_args(self, **kwargs): + return [self.instance.get_vm_desc()] + # intentionally not calling super + + def get_activity_name(self, kwargs): + return create_readable(ugettext_noop("deploy virtual machine"), + ugettext_noop("deploy vm to %(node)s"), + node=self.instance.node) + + @register_operation + class DeployDisksOperation(SubOperationMixin, InstanceOperation): + id = "_deploy_disks" + name = _("deploy disks") + description = _("Deploy all associated disks.") + + def _operation(self): + devnums = list(ascii_lowercase) # a-z + for disk in self.instance.disks.all(): + # assign device numbers + if disk.dev_num in devnums: + devnums.remove(disk.dev_num) + else: + disk.dev_num = devnums.pop(0) + disk.save() + # deploy disk + disk.deploy() + + @register_operation + class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation): + id = "_resume_vm" + name = _("boot virtual machine") + remote_queue = ("vm", "slow") + task = vm_tasks.resume + @register_operation class DestroyOperation(InstanceOperation): - activity_code_suffix = 'destroy' id = 'destroy' name = _("destroy") description = _("Permanently destroy virtual machine, its network " @@ -342,7 +451,7 @@ class DestroyOperation(InstanceOperation): required_perms = () resultant_state = 'DESTROYED' - def _operation(self, activity): + def _operation(self, activity, system): # Destroy networks with activity.sub_activity( 'destroying_net', @@ -352,11 +461,7 @@ class DestroyOperation(InstanceOperation): self.instance.destroy_net() if self.instance.node: - # Delete virtual machine - with activity.sub_activity( - 'destroying_vm', - readable_name=ugettext_noop("destroy virtual machine")): - self.instance.delete_vm() + self.instance._delete_vm(parent_activity=activity) # Destroy disks with activity.sub_activity( @@ -366,7 +471,7 @@ class DestroyOperation(InstanceOperation): # Delete mem. dump if exists try: - self.instance.delete_mem_dump() + self.instance._delete_mem_dump(parent_activity=activity) except: pass @@ -377,18 +482,45 @@ class DestroyOperation(InstanceOperation): self.instance.destroyed_at = timezone.now() self.instance.save() + @register_operation + class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation): + id = "_delete_vm" + name = _("destroy virtual machine") + task = vm_tasks.destroy + # if e.libvirtError and "Domain not found" in str(e): + + @register_operation + class DeleteMemDumpOperation(RemoteOperationMixin, SubOperationMixin, + InstanceOperation): + id = "_delete_mem_dump" + name = _("removing memory dump") + task = storage_tasks.delete_dump + + def _get_remote_queue(self): + return self.instance.mem_dump['datastore'].get_remote_queue_name( + "storage", "fast") + + def _get_remote_args(self, **kwargs): + return [self.instance.mem_dump['path']] + @register_operation -class MigrateOperation(InstanceOperation): - activity_code_suffix = 'migrate' +class MigrateOperation(RemoteInstanceOperation): id = 'migrate' name = _("migrate") - description = _("Move virtual machine to an other worker node with a few " - "seconds of interruption (live migration).") + description = _("Move a running virtual machine to an other worker node " + "keeping its full state.") required_perms = () superuser_required = True accept_states = ('RUNNING', ) async_queue = "localhost.man.slow" + task = vm_tasks.migrate + remote_queue = ("vm", "slow") + remote_timeout = 1000 + + def _get_remote_args(self, to_node, live_migration, **kwargs): + return (super(MigrateOperation, self)._get_remote_args(**kwargs) + + [to_node.host.hostname, live_migration]) def rollback(self, activity): with activity.sub_activity( @@ -396,14 +528,20 @@ class MigrateOperation(InstanceOperation): "redeploy network (rollback)")): self.instance.deploy_net() - def _operation(self, activity, to_node=None, timeout=120): + def _operation(self, activity, to_node=None, live_migration=True): if not to_node: - self.instance.allocate_node(activity) + with activity.sub_activity('scheduling', + readable_name=ugettext_noop( + "schedule")) as sa: + to_node = self.instance.select_node() + sa.result = to_node + try: with activity.sub_activity( 'migrate_vm', readable_name=create_readable( ugettext_noop("migrate to %(node)s"), node=to_node)): - self.instance.migrate_vm(to_node=to_node, timeout=timeout) + super(MigrateOperation, self)._operation( + to_node=to_node, live_migration=live_migration) except Exception as e: if hasattr(e, 'libvirtError'): self.rollback(activity) @@ -418,6 +556,7 @@ class MigrateOperation(InstanceOperation): # Refresh node information self.instance.node = to_node self.instance.save() + # Estabilish network connection (vmdriver) with activity.sub_activity( 'deploying_net', readable_name=ugettext_noop( @@ -426,17 +565,17 @@ class MigrateOperation(InstanceOperation): @register_operation -class RebootOperation(InstanceOperation): - activity_code_suffix = 'reboot' +class RebootOperation(RemoteInstanceOperation): id = 'reboot' name = _("reboot") description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del " "signal to its console.") required_perms = () accept_states = ('RUNNING', ) + task = vm_tasks.reboot - def _operation(self, activity, timeout=5): - self.instance.reboot_vm(timeout=timeout) + def _operation(self, activity): + super(RebootOperation, self)._operation() if self.instance.has_agent: activity.sub_activity('os_boot', readable_name=ugettext_noop( "wait operating system loading"), interruptible=True) @@ -444,7 +583,6 @@ class RebootOperation(InstanceOperation): @register_operation class RemoveInterfaceOperation(InstanceOperation): - activity_code_suffix = 'remove_interface' id = 'remove_interface' name = _("remove interface") description = _("Remove the specified network interface and erase IP " @@ -455,11 +593,8 @@ class RemoveInterfaceOperation(InstanceOperation): def _operation(self, activity, user, system, interface): if self.instance.is_running: - with activity.sub_activity( - 'detach_network', - readable_name=ugettext_noop("detach network") - ): - self.instance.detach_network(interface) + self.instance._detach_network(interface=interface, + parent_activity=activity) interface.shutdown() interface.destroy() @@ -472,7 +607,6 @@ class RemoveInterfaceOperation(InstanceOperation): @register_operation class RemoveDiskOperation(InstanceOperation): - activity_code_suffix = 'remove_disk' id = 'remove_disk' name = _("remove disk") description = _("Remove the specified disk from the virtual machine, and " @@ -482,15 +616,12 @@ class RemoveDiskOperation(InstanceOperation): def _operation(self, activity, user, system, disk): if self.instance.is_running and disk.type not in ["iso"]: - with activity.sub_activity( - 'detach_disk', - readable_name=ugettext_noop('detach disk') - ): - self.instance.detach_disk(disk) + self.instance._detach_disk(disk=disk, parent_activity=activity) with activity.sub_activity( 'destroy_disk', readable_name=ugettext_noop('destroy disk') ): + disk.destroy() return self.instance.disks.remove(disk) def get_activity_name(self, kwargs): @@ -499,16 +630,16 @@ class RemoveDiskOperation(InstanceOperation): @register_operation -class ResetOperation(InstanceOperation): - activity_code_suffix = 'reset' +class ResetOperation(RemoteInstanceOperation): id = 'reset' name = _("reset") description = _("Cold reboot virtual machine (power cycle).") required_perms = () accept_states = ('RUNNING', ) + task = vm_tasks.reset - def _operation(self, activity, timeout=5): - self.instance.reset_vm(timeout=timeout) + def _operation(self, activity): + super(ResetOperation, self)._operation() if self.instance.has_agent: activity.sub_activity('os_boot', readable_name=ugettext_noop( "wait operating system loading"), interruptible=True) @@ -516,7 +647,6 @@ class ResetOperation(InstanceOperation): @register_operation class SaveAsTemplateOperation(InstanceOperation): - activity_code_suffix = 'save_as_template' id = 'save_as_template' name = _("save as template") description = _("Save virtual machine as a template so they can be shared " @@ -548,8 +678,13 @@ class SaveAsTemplateOperation(InstanceOperation): for disk in self.disks: disk.destroy() - def _operation(self, activity, user, system, timeout=300, name=None, - with_shutdown=True, task=None, **kwargs): + def _operation(self, activity, user, system, name=None, + with_shutdown=True, clone=False, task=None, **kwargs): + try: + self.instance._cleanup(parent_activity=activity, user=user) + except: + pass + if with_shutdown: try: ShutdownOperation(self.instance).call(parent_activity=activity, @@ -599,6 +734,8 @@ class SaveAsTemplateOperation(InstanceOperation): tmpl = InstanceTemplate(**params) tmpl.full_clean() # Avoiding database errors. tmpl.save() + if clone: + tmpl.clone_acl(self.instance.template) try: tmpl.disks.add(*self.disks) # create interface templates @@ -612,8 +749,8 @@ class SaveAsTemplateOperation(InstanceOperation): @register_operation -class ShutdownOperation(InstanceOperation): - activity_code_suffix = 'shutdown' +class ShutdownOperation(AbortableRemoteOperationMixin, + RemoteInstanceOperation): id = 'shutdown' name = _("shutdown") description = _("Try to halt virtual machine by a standard ACPI signal, " @@ -624,9 +761,12 @@ class ShutdownOperation(InstanceOperation): required_perms = () accept_states = ('RUNNING', ) resultant_state = 'STOPPED' + task = vm_tasks.shutdown + remote_queue = ("vm", "slow") + remote_timeout = 120 - def _operation(self, task=None): - self.instance.shutdown_vm(task=task) + def _operation(self, task): + super(ShutdownOperation, self)._operation(task=task) self.instance.yield_node() def on_abort(self, activity, error): @@ -643,7 +783,6 @@ class ShutdownOperation(InstanceOperation): @register_operation class ShutOffOperation(InstanceOperation): - activity_code_suffix = 'shut_off' id = 'shut_off' name = _("shut off") description = _("Forcibly halt a virtual machine without notifying the " @@ -654,7 +793,7 @@ class ShutOffOperation(InstanceOperation): "operation is the same as interrupting the power supply " "of a physical machine.") required_perms = () - accept_states = ('RUNNING', ) + accept_states = ('RUNNING', 'PAUSED') resultant_state = 'STOPPED' def _operation(self, activity): @@ -662,16 +801,12 @@ class ShutOffOperation(InstanceOperation): with activity.sub_activity('shutdown_net'): self.instance.shutdown_net() - # Delete virtual machine - with activity.sub_activity('delete_vm'): - self.instance.delete_vm() - + self.instance._delete_vm(parent_activity=activity) self.instance.yield_node() @register_operation class SleepOperation(InstanceOperation): - activity_code_suffix = 'sleep' id = 'sleep' name = _("sleep") description = _("Suspend virtual machine. This means the machine is " @@ -697,25 +832,30 @@ class SleepOperation(InstanceOperation): else: activity.resultant_state = 'ERROR' - def _operation(self, activity, timeout=240): - # Destroy networks - with activity.sub_activity('shutdown_net', readable_name=ugettext_noop( - "shutdown network")): + def _operation(self, activity, system): + with activity.sub_activity('shutdown_net', + readable_name=ugettext_noop( + "shutdown network")): self.instance.shutdown_net() + self.instance._suspend_vm(parent_activity=activity) + self.instance.yield_node() - # Suspend vm - with activity.sub_activity('suspending', - readable_name=ugettext_noop( - "suspend virtual machine")): - self.instance.suspend_vm(timeout=timeout) + @register_operation + class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation): + id = "_suspend_vm" + name = _("suspend virtual machine") + task = vm_tasks.sleep + remote_queue = ("vm", "slow") + remote_timeout = 1000 - self.instance.yield_node() - # VNC port needs to be kept + def _get_remote_args(self, **kwargs): + return (super(SleepOperation.SuspendVmOperation, self) + ._get_remote_args(**kwargs) + + [self.instance.mem_dump['path']]) @register_operation class WakeUpOperation(InstanceOperation): - activity_code_suffix = 'wake_up' id = 'wake_up' name = _("wake up") description = _("Wake up sleeping (suspended) virtual machine. This will " @@ -724,6 +864,7 @@ class WakeUpOperation(InstanceOperation): required_perms = () accept_states = ('SUSPENDED', ) resultant_state = 'RUNNING' + async_queue = "localhost.man.slow" def is_preferred(self): return self.instance.status == self.instance.STATUS.SUSPENDED @@ -734,16 +875,13 @@ class WakeUpOperation(InstanceOperation): else: activity.resultant_state = 'ERROR' - def _operation(self, activity, timeout=60): + def _operation(self, activity): # Schedule vm self.instance.allocate_vnc_port() - self.instance.allocate_node(activity) + self.instance.allocate_node() # Resume vm - with activity.sub_activity( - 'resuming', readable_name=ugettext_noop( - "resume virtual machine")): - self.instance.wake_up_vm(timeout=timeout) + self.instance._wake_up_vm(parent_activity=activity) # Estabilish network connection (vmdriver) with activity.sub_activity( @@ -756,10 +894,22 @@ class WakeUpOperation(InstanceOperation): except: pass + @register_operation + class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation): + id = "_wake_up_vm" + name = _("resume virtual machine") + task = vm_tasks.wake_up + remote_queue = ("vm", "slow") + remote_timeout = 1000 + + def _get_remote_args(self, **kwargs): + return (super(WakeUpOperation.WakeUpVmOperation, self) + ._get_remote_args(**kwargs) + + [self.instance.mem_dump['path']]) + @register_operation class RenewOperation(InstanceOperation): - activity_code_suffix = 'renew' id = 'renew' name = _("renew") description = _("Virtual machines are suspended and destroyed after they " @@ -794,7 +944,6 @@ class RenewOperation(InstanceOperation): @register_operation class ChangeStateOperation(InstanceOperation): - activity_code_suffix = 'emergency_change_state' id = 'emergency_change_state' name = _("emergency state change") description = _("Change the virtual machine state to NOSTATE. This " @@ -824,7 +973,6 @@ class ChangeStateOperation(InstanceOperation): @register_operation class RedeployOperation(InstanceOperation): - activity_code_suffix = 'redeploy' id = 'redeploy' name = _("redeploy") description = _("Change the virtual machine state to NOSTATE " @@ -878,17 +1026,17 @@ class NodeOperation(Operation): "parent activity does not match the user " "provided as parameter.") - return parent.create_sub(code_suffix=self.activity_code_suffix, - readable_name=name) + return parent.create_sub( + code_suffix=self.get_activity_code_suffix(), + readable_name=name) else: - return NodeActivity.create(code_suffix=self.activity_code_suffix, - node=self.node, user=user, - readable_name=name) + return NodeActivity.create( + code_suffix=self.get_activity_code_suffix(), node=self.node, + user=user, readable_name=name) @register_operation class ResetNodeOperation(NodeOperation): - activity_code_suffix = 'reset' id = 'reset' name = _("reset") description = _("Disable missing node and redeploy all instances " @@ -917,7 +1065,6 @@ class ResetNodeOperation(NodeOperation): @register_operation class FlushOperation(NodeOperation): - activity_code_suffix = 'flush' id = 'flush' name = _("flush") description = _("Passivate node and move all instances to other ones.") @@ -938,7 +1085,6 @@ class FlushOperation(NodeOperation): @register_operation class ActivateOperation(NodeOperation): - activity_code_suffix = 'activate' id = 'activate' name = _("activate") description = _("Make node active, i.e. scheduler is allowed to deploy " @@ -959,7 +1105,6 @@ class ActivateOperation(NodeOperation): @register_operation class PassivateOperation(NodeOperation): - activity_code_suffix = 'passivate' id = 'passivate' name = _("passivate") description = _("Make node passive, i.e. scheduler is denied to deploy " @@ -981,7 +1126,6 @@ class PassivateOperation(NodeOperation): @register_operation class DisableOperation(NodeOperation): - activity_code_suffix = 'disable' id = 'disable' name = _("disable") description = _("Disable node.") @@ -1005,8 +1149,7 @@ class DisableOperation(NodeOperation): @register_operation -class ScreenshotOperation(InstanceOperation): - activity_code_suffix = 'screenshot' +class ScreenshotOperation(RemoteInstanceOperation): id = 'screenshot' name = _("screenshot") description = _("Get a screenshot about the virtual machine's console. A " @@ -1015,14 +1158,11 @@ class ScreenshotOperation(InstanceOperation): acl_level = "owner" required_perms = () accept_states = ('RUNNING', ) - - def _operation(self): - return self.instance.get_screenshot(timeout=20) + task = vm_tasks.screenshot @register_operation class RecoverOperation(InstanceOperation): - activity_code_suffix = 'recover' id = 'recover' name = _("recover") description = _("Try to recover virtual machine disks from destroyed " @@ -1050,7 +1190,6 @@ class RecoverOperation(InstanceOperation): @register_operation class ResourcesOperation(InstanceOperation): - activity_code_suffix = 'Resources change' id = 'resources_change' name = _("resources change") description = _("Change resources of a stopped virtual machine.") @@ -1076,28 +1215,8 @@ class ResourcesOperation(InstanceOperation): ) -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) - - @register_operation -class PasswordResetOperation(EnsureAgentMixin, InstanceOperation): - activity_code_suffix = 'password_reset' +class PasswordResetOperation(RemoteAgentOperation): id = 'password_reset' name = _("password reset") description = _("Generate and set a new login password on the virtual " @@ -1106,19 +1225,237 @@ class PasswordResetOperation(EnsureAgentMixin, InstanceOperation): "logging in as other settings are possible to prevent " "it.") acl_level = "owner" + task = agent_tasks.change_password required_perms = () - def _operation(self): - self.instance.pw = pwgen() - queue = self.instance.get_remote_queue_name("agent") - agent_tasks.change_password.apply_async( - queue=queue, args=(self.instance.vm_name, self.instance.pw)) + def _get_remote_args(self, password, **kwargs): + return (super(PasswordResetOperation, self)._get_remote_args(**kwargs) + + [password]) + + def _operation(self, password=None): + if not password: + password = pwgen() + super(PasswordResetOperation, self)._operation(password=password) + self.instance.pw = password self.instance.save() @register_operation +class AgentStartedOperation(InstanceOperation): + id = 'agent_started' + name = _("agent") + acl_level = "owner" + required_perms = () + concurrency_check = False + + @classmethod + def get_activity_code_suffix(cls): + return 'agent' + + @property + def initialized(self): + return self.instance.activity_log.filter( + activity_code='vm.Instance.agent._cleanup').exists() + + def measure_boot_time(self): + if not self.instance.template: + return + + deploy_time = InstanceActivity.objects.filter( + instance=self.instance, activity_code="vm.Instance.deploy" + ).latest("finished").finished + + total_boot_time = (timezone.now() - deploy_time).total_seconds() + + Client().send([ + "template.%(pk)d.boot_time %(val)f %(time)s" % { + 'pk': self.instance.template.pk, + 'val': total_boot_time, + 'time': time.time(), + } + ]) + + def finish_agent_wait(self): + for i in InstanceActivity.objects.filter( + (Q(activity_code__endswith='.os_boot') | + Q(activity_code__endswith='.agent_wait')), + instance=self.instance, finished__isnull=True): + i.finish(True) + + def _operation(self, user, activity, old_version=None, agent_system=None): + with activity.sub_activity('starting', concurrency_check=False, + readable_name=ugettext_noop('starting')): + pass + + self.finish_agent_wait() + + self.instance._change_ip(parent_activity=activity) + self.instance._restart_networking(parent_activity=activity) + + new_version = settings.AGENT_VERSION + if new_version and old_version and new_version != old_version: + try: + self.instance.update_agent( + parent_activity=activity, agent_system=agent_system) + except TimeoutError: + pass + else: + activity.sub_activity( + 'agent_wait', readable_name=ugettext_noop( + "wait agent restarting"), interruptible=True) + return # agent is going to restart + + if not self.initialized: + try: + self.measure_boot_time() + except: + logger.exception('Unhandled error in measure_boot_time()') + self.instance._cleanup(parent_activity=activity) + self.instance.password_reset( + parent_activity=activity, password=self.instance.pw) + self.instance._set_time(parent_activity=activity) + self.instance._set_hostname(parent_activity=activity) + + @register_operation + class CleanupOperation(SubOperationMixin, RemoteAgentOperation): + id = '_cleanup' + name = _("cleanup") + task = agent_tasks.cleanup + + @register_operation + class SetTimeOperation(SubOperationMixin, RemoteAgentOperation): + id = '_set_time' + name = _("set time") + task = agent_tasks.set_time + + def _get_remote_args(self, **kwargs): + cls = AgentStartedOperation.SetTimeOperation + return (super(cls, self)._get_remote_args(**kwargs) + + [time.time()]) + + @register_operation + class SetHostnameOperation(SubOperationMixin, RemoteAgentOperation): + id = '_set_hostname' + name = _("set hostname") + task = agent_tasks.set_hostname + + def _get_remote_args(self, **kwargs): + cls = AgentStartedOperation.SetHostnameOperation + return (super(cls, self)._get_remote_args(**kwargs) + + [self.instance.short_hostname]) + + @register_operation + class RestartNetworkingOperation(SubOperationMixin, RemoteAgentOperation): + id = '_restart_networking' + name = _("restart networking") + task = agent_tasks.restart_networking + + @register_operation + class ChangeIpOperation(SubOperationMixin, RemoteAgentOperation): + id = '_change_ip' + name = _("change ip") + task = agent_tasks.change_ip + + def _get_remote_args(self, **kwargs): + hosts = Host.objects.filter(interface__instance=self.instance) + interfaces = {str(host.mac): host.get_network_config() + for host in hosts} + cls = AgentStartedOperation.ChangeIpOperation + return (super(cls, self)._get_remote_args(**kwargs) + + [interfaces, settings.FIREWALL_SETTINGS['rdns_ip']]) + + +@register_operation +class UpdateAgentOperation(RemoteAgentOperation): + id = 'update_agent' + name = _("update agent") + acl_level = "owner" + required_perms = () + + def get_activity_name(self, kwargs): + return create_readable( + ugettext_noop('update agent to %(version)s'), + version=settings.AGENT_VERSION) + + @staticmethod + def create_linux_tar(): + def exclude(tarinfo): + ignored = ('./.', './misc', './windows') + if any(tarinfo.name.startswith(x) for x in ignored): + return None + else: + return tarinfo + + f = StringIO() + + with TarFile.open(fileobj=f, mode='w:gz') as tar: + agent_path = os.path.join(settings.AGENT_DIR, "agent-linux") + tar.add(agent_path, arcname='.', filter=exclude) + + version_fileobj = StringIO(settings.AGENT_VERSION) + version_info = TarInfo(name='version.txt') + version_info.size = len(version_fileobj.buf) + tar.addfile(version_info, version_fileobj) + + return encodestring(f.getvalue()).replace('\n', '') + + @staticmethod + def create_windows_tar(): + f = StringIO() + + agent_path = os.path.join(settings.AGENT_DIR, "agent-win") + with TarFile.open(fileobj=f, mode='w|gz') as tar: + tar.add(agent_path, arcname='.') + + version_fileobj = StringIO(settings.AGENT_VERSION) + version_info = TarInfo(name='version.txt') + version_info.size = len(version_fileobj.buf) + tar.addfile(version_info, version_fileobj) + + return encodestring(f.getvalue()).replace('\n', '') + + def _operation(self, user, activity, agent_system): + queue = self._get_remote_queue() + instance = self.instance + if agent_system == "Windows": + executable = os.listdir( + os.path.join(settings.AGENT_DIR, "agent-win"))[0] + data = self.create_windows_tar() + elif agent_system == "Linux": + executable = "" + data = self.create_linux_tar() + else: + # Legacy update method + executable = "" + return agent_tasks.update_legacy.apply_async( + queue=queue, + args=(instance.vm_name, self.create_linux_tar()) + ).get(timeout=60) + + checksum = md5(data).hexdigest() + chunk_size = 1024 * 1024 + chunk_number = 0 + index = 0 + filename = settings.AGENT_VERSION + ".tar" + while True: + chunk = data[index:index+chunk_size] + if chunk: + agent_tasks.append.apply_async( + queue=queue, + args=(instance.vm_name, chunk, + filename, chunk_number)).get(timeout=60) + index = index + chunk_size + chunk_number = chunk_number + 1 + else: + agent_tasks.update.apply_async( + queue=queue, + args=(instance.vm_name, filename, executable, checksum) + ).get(timeout=60) + break + + +@register_operation class MountStoreOperation(EnsureAgentMixin, InstanceOperation): - activity_code_suffix = 'mount_store' id = 'mount_store' name = _("mount store") description = _( @@ -1143,3 +1480,61 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation): password = user.profile.smb_password agent_tasks.mount_store.apply_async( queue=queue, args=(inst.vm_name, host, username, password)) + + +class AbstractDiskOperation(SubOperationMixin, RemoteInstanceOperation): + required_perms = () + + def _get_remote_args(self, disk, **kwargs): + return (super(AbstractDiskOperation, self)._get_remote_args(**kwargs) + + [disk.get_vmdisk_desc()]) + + +@register_operation +class AttachDisk(AbstractDiskOperation): + id = "_attach_disk" + name = _("attach disk") + task = vm_tasks.attach_disk + + +class DetachMixin(object): + def _operation(self, activity, **kwargs): + try: + super(DetachMixin, self)._operation(**kwargs) + except Exception as e: + if hasattr(e, "libvirtError") and "not found" in unicode(e): + activity.result = create_readable( + ugettext_noop("Resource was not found."), + ugettext_noop("Resource was not found. %(exception)s"), + exception=unicode(e)) + else: + raise + + +@register_operation +class DetachDisk(DetachMixin, AbstractDiskOperation): + id = "_detach_disk" + name = _("detach disk") + task = vm_tasks.detach_disk + + +class AbstractNetworkOperation(SubOperationMixin, RemoteInstanceOperation): + required_perms = () + + def _get_remote_args(self, interface, **kwargs): + return (super(AbstractNetworkOperation, self) + ._get_remote_args(**kwargs) + [interface.get_vmnetwork_desc()]) + + +@register_operation +class AttachNetwork(AbstractNetworkOperation): + id = "_attach_network" + name = _("attach network") + task = vm_tasks.attach_network + + +@register_operation +class DetachNetwork(DetachMixin, AbstractNetworkOperation): + id = "_detach_network" + name = _("detach network") + task = vm_tasks.detach_network diff --git a/circle/vm/tasks/agent_tasks.py b/circle/vm/tasks/agent_tasks.py index 5528735..db5ad56 100644 --- a/circle/vm/tasks/agent_tasks.py +++ b/circle/vm/tasks/agent_tasks.py @@ -53,8 +53,18 @@ def start_access_server(vm): pass +@celery.task(name='agent.update_legacy') +def update_legacy(vm, data, executable=None): + pass + + +@celery.task(name='agent.append') +def append(vm, data, filename, chunk_number): + pass + + @celery.task(name='agent.update') -def update(vm, data): +def update(vm, filename, executable, checksum): pass diff --git a/circle/vm/tasks/local_agent_tasks.py b/circle/vm/tasks/local_agent_tasks.py index d055eb5..90e1134 100644 --- a/circle/vm/tasks/local_agent_tasks.py +++ b/circle/vm/tasks/local_agent_tasks.py @@ -15,169 +15,26 @@ # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see . -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, change_ip) -from firewall.models import Host - -import time -from base64 import encodestring -from StringIO import StringIO -from tarfile import TarFile, TarInfo -from django.conf import settings -from django.db.models import Q -from django.utils import timezone from django.utils.translation import ugettext_noop -from celery.result import TimeoutError -from monitor.client import Client - - -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('change_password', - readable_name=ugettext_noop('change password')): - change_password.apply_async(queue=queue, args=(vm, instance.pw)) - with act.sub_activity('set_time', readable_name=ugettext_noop('set time')): - set_time.apply_async(queue=queue, args=(vm, time.time())) - with act.sub_activity('set_hostname', - readable_name=ugettext_noop('set hostname')): - set_hostname.apply_async( - queue=queue, args=(vm, instance.short_hostname)) - - -def send_networking_commands(instance, act): - queue = instance.get_remote_queue_name("agent") - with act.sub_activity('change_ip', - readable_name=ugettext_noop('change ip')): - change_ip.apply_async(queue=queue, args=( - instance.vm_name, ) + get_network_configs(instance)) - with act.sub_activity('restart_networking', - readable_name=ugettext_noop('restart networking')): - restart_networking.apply_async(queue=queue, args=(instance.vm_name, )) - -def create_agent_tar(): - def exclude(tarinfo): - if tarinfo.name.startswith('./.git'): - return None - else: - return tarinfo - - f = StringIO() - - with TarFile.open(fileobj=f, mode='w|gz') as tar: - tar.add(settings.AGENT_DIR, arcname='.', filter=exclude) - - version_fileobj = StringIO(settings.AGENT_VERSION) - version_info = TarInfo(name='version.txt') - version_info.size = len(version_fileobj.buf) - tar.addfile(version_info, version_fileobj) - - return encodestring(f.getvalue()).replace('\n', '') +from manager.mancelery import celery @celery.task -def agent_started(vm, version=None): - from vm.models import Instance, instance_activity, InstanceActivity +def agent_started(vm, version=None, system=None): + from vm.models import Instance instance = Instance.objects.get(id=int(vm.split('-')[-1])) - queue = instance.get_remote_queue_name("agent") - initialized = instance.activity_log.filter( - activity_code='vm.Instance.agent.cleanup').exists() - - with instance_activity(code_suffix='agent', - readable_name=ugettext_noop('agent'), - concurrency_check=False, - instance=instance) as act: - with act.sub_activity('starting', - readable_name=ugettext_noop('starting')): - pass - - for i in InstanceActivity.objects.filter( - (Q(activity_code__endswith='.os_boot') | - Q(activity_code__endswith='.agent_wait')), - instance=instance, finished__isnull=True): - i.finish(True) - - if version and version != settings.AGENT_VERSION: - try: - update_agent(instance, act) - except TimeoutError: - pass - else: - act.sub_activity('agent_wait', readable_name=ugettext_noop( - "wait agent restarting"), interruptible=True) - return # agent is going to restart - - if not initialized: - measure_boot_time(instance) - send_init_commands(instance, act) - - send_networking_commands(instance, act) - with act.sub_activity('start_access_server', - readable_name=ugettext_noop( - 'start access server')): - start_access_server.apply_async(queue=queue, args=(vm, )) - - -def measure_boot_time(instance): - if not instance.template: - return - - from vm.models import InstanceActivity - deploy_time = InstanceActivity.objects.filter( - instance=instance, activity_code="vm.Instance.deploy" - ).latest("finished").finished - - total_boot_time = (timezone.now() - deploy_time).total_seconds() - - Client().send([ - "template.%(pk)d.boot_time %(val)f %(time)s" % { - 'pk': instance.template.pk, - 'val': total_boot_time, - 'time': time.time(), - } - ]) + instance.agent_started( + user=instance.owner, old_version=version, agent_system=system) @celery.task def agent_stopped(vm): from vm.models import Instance, InstanceActivity instance = Instance.objects.get(id=int(vm.split('-')[-1])) - qs = InstanceActivity.objects.filter(instance=instance, - activity_code='vm.Instance.agent') + qs = InstanceActivity.objects.filter( + instance=instance, activity_code='vm.Instance.agent') act = qs.latest('id') - with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')): + with act.sub_activity('stopping', concurrency_check=False, + readable_name=ugettext_noop('stopping')): pass - - -def get_network_configs(instance): - interfaces = {} - for host in Host.objects.filter(interface__instance=instance): - interfaces[str(host.mac)] = host.get_network_config() - return (interfaces, settings.FIREWALL_SETTINGS['rdns_ip']) - - -def update_agent(instance, act=None): - if act: - act = act.sub_activity( - 'update', - readable_name=create_readable( - ugettext_noop('update to %(version)s'), - version=settings.AGENT_VERSION)) - else: - from vm.models import instance_activity - act = instance_activity( - code_suffix='agent.update', instance=instance, - readable_name=create_readable( - ugettext_noop('update agent to %(version)s'), - version=settings.AGENT_VERSION)) - with act: - queue = instance.get_remote_queue_name("agent") - update.apply_async( - queue=queue, - args=(instance.vm_name, create_agent_tar())).get(timeout=10) diff --git a/circle/vm/tests/test_models.py b/circle/vm/tests/test_models.py index e0c7dee..8f9ff41 100644 --- a/circle/vm/tests/test_models.py +++ b/circle/vm/tests/test_models.py @@ -29,7 +29,8 @@ from ..models import ( ) from ..models.instance import find_unused_port, ActivityInProgressError from ..operations import ( - DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, + RemoteOperationMixin, DeployOperation, DestroyOperation, FlushOperation, + MigrateOperation, ) @@ -89,7 +90,7 @@ class InstanceTestCase(TestCase): self.assertFalse(inst.save.called) def test_destroy_sets_destroyed(self): - inst = Mock(destroyed_at=None, spec=Instance, + inst = Mock(destroyed_at=None, spec=Instance, _delete_vm=Mock(), InstanceDestroyedError=Instance.InstanceDestroyedError) inst.node = MagicMock(spec=Node) inst.disks.all.return_value = [] @@ -105,7 +106,8 @@ class InstanceTestCase(TestCase): inst.node = MagicMock(spec=Node) inst.status = 'RUNNING' migrate_op = MigrateOperation(inst) - with patch('vm.models.instance.vm_tasks.migrate') as migr: + with patch('vm.operations.vm_tasks.migrate') as migr, \ + patch.object(RemoteOperationMixin, "_operation"): act = MagicMock() with patch.object(MigrateOperation, 'create_activity', return_value=act): @@ -121,7 +123,8 @@ class InstanceTestCase(TestCase): inst.node = MagicMock(spec=Node) inst.status = 'RUNNING' migrate_op = MigrateOperation(inst) - with patch('vm.models.instance.vm_tasks.migrate') as migr: + with patch('vm.operations.vm_tasks.migrate') as migr, \ + patch.object(RemoteOperationMixin, "_operation"): inst.select_node.side_effect = AssertionError act = MagicMock() with patch.object(MigrateOperation, 'create_activity', @@ -138,19 +141,22 @@ class InstanceTestCase(TestCase): inst.status = 'RUNNING' e = Exception('abc') setattr(e, 'libvirtError', '') - inst.migrate_vm.side_effect = e migrate_op = MigrateOperation(inst) - with patch('vm.models.instance.vm_tasks.migrate') as migr: + migrate_op.rollback = Mock() + with patch('vm.operations.vm_tasks.migrate') as migr, \ + patch.object(RemoteOperationMixin, '_operation') as remop: act = MagicMock() + remop.side_effect = e with patch.object(MigrateOperation, 'create_activity', return_value=act): self.assertRaises(Exception, migrate_op, system=True) + remop.assert_called() migr.apply_async.assert_called() self.assertIn(call.sub_activity( - u'rollback_net', readable_name=u'redeploy network (rollback)'), - act.mock_calls) - inst.allocate_node.assert_called() + u'scheduling', readable_name=u'schedule'), act.mock_calls) + migrate_op.rollback.assert_called() + inst.select_node.assert_called() def test_status_icon(self): inst = MagicMock(spec=Instance) diff --git a/circle/vm/tests/test_operations.py b/circle/vm/tests/test_operations.py index eaa5aa2..f5eca95 100644 --- a/circle/vm/tests/test_operations.py +++ b/circle/vm/tests/test_operations.py @@ -16,9 +16,10 @@ # with CIRCLE. If not, see . from django.test import TestCase +from mock import MagicMock from common.operations import operation_registry_name as op_reg_name -from vm.models import Instance, Node +from vm.models import Instance, InstanceActivity, Node from vm.operations import ( DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, RebootOperation, ResetOperation, SaveAsTemplateOperation, @@ -45,6 +46,22 @@ class MigrateOperationTestCase(TestCase): def test_operation_registered(self): assert MigrateOperation.id in getattr(Instance, op_reg_name) + def test_operation_wo_to_node_param(self): + class MigrateException(Exception): + pass + + inst = MagicMock(spec=Instance) + act = MagicMock(spec=InstanceActivity) + op = MigrateOperation(inst) + op._get_remote_args = MagicMock(side_effect=MigrateException()) + inst.select_node = MagicMock(return_value='test') + self.assertRaises( + MigrateException, op._operation, + act, to_node=None) + assert inst.select_node.called + op._get_remote_args.assert_called_once_with( + to_node='test', live_migration=True) + class RebootOperationTestCase(TestCase): def test_operation_registered(self): diff --git a/miscellaneous/mancelery.conf b/miscellaneous/mancelery.conf index a6f63c6..e3dd802 100644 --- a/miscellaneous/mancelery.conf +++ b/miscellaneous/mancelery.conf @@ -6,9 +6,14 @@ respawn limit 30 30 setgid cloud setuid cloud +kill timeout 360 +kill signal SIGTERM + 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 10 + ./manage.py celery -f --app=manager.mancelery purge + exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 3 end script + diff --git a/miscellaneous/moncelery.conf b/miscellaneous/moncelery.conf index ca00325..7c107c1 100644 --- a/miscellaneous/moncelery.conf +++ b/miscellaneous/moncelery.conf @@ -3,6 +3,7 @@ description "CIRCLE moncelery for monitoring jobs" respawn respawn limit 30 30 + setgid cloud setuid cloud @@ -10,5 +11,7 @@ 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 + ./manage.py celery -f --app=manager.moncelery purge + exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 2 end script + diff --git a/miscellaneous/slowcelery.conf b/miscellaneous/slowcelery.conf index b4fdc75..e8c16c1 100644 --- a/miscellaneous/slowcelery.conf +++ b/miscellaneous/slowcelery.conf @@ -1,4 +1,4 @@ -description "CIRCLE mancelery for slow jobs" +description "CIRCLE slowcelery for resource intensive or long jobs" respawn respawn limit 30 30 @@ -6,9 +6,15 @@ respawn limit 30 30 setgid cloud setuid cloud +kill timeout 360 +kill signal INT + + 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 + ./manage.py celery -f --app=manager.slowcelery purge + exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 1 end script +