diff --git a/.gitignore b/.gitignore index 75a1da9..6f4d056 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 a47067a..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): diff --git a/circle/circle/settings/local.py b/circle/circle/settings/local.py index 4336c71..1d5cb37 100644 --- a/circle/circle/settings/local.py +++ b/circle/circle/settings/local.py @@ -64,6 +64,13 @@ CACHES = { ########## END CACHE CONFIGURATION +########## ROSETTA CONFIGURATION +INSTALLED_APPS += ( + 'rosetta', +) +########## END ROSETTA CONFIGURATION + + ########## TOOLBAR CONFIGURATION # https://github.com/django-debug-toolbar/django-debug-toolbar#installation if get_env_variable('DJANGO_TOOLBAR', 'FALSE') == 'TRUE': diff --git a/circle/circle/urls.py b/circle/circle/urls.py index b049a1a..2671c23 100644 --- a/circle/circle/urls.py +++ b/circle/circle/urls.py @@ -18,9 +18,11 @@ from django.conf.urls import patterns, include, url from django.views.generic import TemplateView +from django.conf import settings from django.contrib import admin -from django.shortcuts import redirect from django.core.urlresolvers import reverse +from django.shortcuts import redirect + from circle.settings.base import get_env_variable from dashboard.views import circle_login, HelpView @@ -72,6 +74,13 @@ urlpatterns = patterns( ) +if 'rosetta' in settings.INSTALLED_APPS: + urlpatterns += patterns( + '', + url(r'^rosetta/', include('rosetta.urls')), + ) + + if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': urlpatterns += patterns( '', 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/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/dashboard/forms.py b/circle/dashboard/forms.py index b2a8f4a..ad1b0ec 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -40,7 +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.html import escape, format_html from django.utils.translation import ugettext_lazy as _ from sizefield.widgets import FileSizeWidget from django.core.urlresolvers import reverse_lazy @@ -71,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): @@ -81,11 +79,26 @@ class VmSaveForm(forms.Form): helper.form_tag = False 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): @@ -193,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})) @@ -232,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: @@ -242,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) @@ -261,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): @@ -278,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( @@ -345,6 +355,7 @@ class HostForm(forms.ModelForm): ), ), ) + return helper class Meta: model = Host @@ -725,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 " @@ -745,16 +756,15 @@ 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")) + 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') @@ -766,7 +776,7 @@ class VmMigrateForm(forms.Form): widget=forms.RadioSelect(), label=_("Node"))) -class VmStateChangeForm(forms.Form): +class VmStateChangeForm(OperationForm): interrupt = forms.BooleanField(required=False, label=_( "Forcibly interrupt all running activities."), @@ -785,25 +795,13 @@ 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'), @@ -823,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 ' @@ -863,8 +855,7 @@ 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(_("<label>Disk:</label> %s") % escape(self.disk)), @@ -872,7 +863,7 @@ class VmDiskResizeForm(forms.Form): return helper -class VmDiskRemoveForm(forms.Form): +class VmDiskRemoveForm(OperationForm): def __init__(self, *args, **kwargs): choices = kwargs.pop('choices') self.disk = kwargs.pop('default') @@ -887,8 +878,7 @@ class VmDiskRemoveForm(forms.Form): @property def helper(self): - helper = FormHelper(self) - helper.form_tag = False + helper = super(VmDiskRemoveForm, self).helper if self.disk: helper.layout = Layout( AnyTag( @@ -901,16 +891,10 @@ class VmDiskRemoveForm(forms.Form): return helper -class VmDownloadDiskForm(forms.Form): +class VmDownloadDiskForm(OperationForm): name = forms.CharField(max_length=100, label=_("Name"), required=False) url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) - @property - def helper(self): - helper = FormHelper(self) - helper.form_tag = False - return helper - def clean(self): cleaned_data = super(VmDownloadDiskForm, self).clean() if not cleaned_data['name']: @@ -924,7 +908,7 @@ class VmDownloadDiskForm(forms.Form): return cleaned_data -class VmAddInterfaceForm(forms.Form): +class VmAddInterfaceForm(OperationForm): def __init__(self, *args, **kwargs): choices = kwargs.pop('choices') super(VmAddInterfaceForm, self).__init__(*args, **kwargs) @@ -936,10 +920,68 @@ class VmAddInterfaceForm(forms.Form): field.empty_label = _('No more networks.') self.fields['vlan'] = field + +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 VmPortRemoveForm(OperationForm): + def __init__(self, *args, **kwargs): + choices = kwargs.pop('choices') + self.rule = kwargs.pop('default') + + super(VmPortRemoveForm, self).__init__(*args, **kwargs) + + self.fields.insert(0, 'rule', forms.ModelChoiceField( + queryset=choices, initial=self.rule, required=True, + empty_label=None, label=_('Port'))) + if self.rule: + self.fields['rule'].widget = HiddenInput() + + +class VmPortAddForm(OperationForm): + port = forms.IntegerField(required=True, label=_('Port'), + min_value=1, max_value=65535) + proto = forms.ChoiceField((('tcp', 'tcp'), ('udp', 'udp')), + required=True, label=_('Protocol')) + + def __init__(self, *args, **kwargs): + choices = kwargs.pop('choices') + self.host = kwargs.pop('default') + + super(VmPortAddForm, self).__init__(*args, **kwargs) + + self.fields.insert(0, 'host', forms.ModelChoiceField( + queryset=choices, initial=self.host, required=True, + empty_label=None, label=_('Host'))) + if self.host: + self.fields['host'].widget = HiddenInput() + @property def helper(self): - helper = FormHelper(self) - helper.form_tag = False + helper = super(VmPortAddForm, self).helper + if self.host: + helper.layout = Layout( + AnyTag( + "div", + HTML(format_html( + _("<label>Host:</label> {0}"), self.host)), + css_class="form-group", + ), + Field("host"), + Field("proto"), + Field("port"), + ) return helper diff --git a/circle/dashboard/static/dashboard/bootstrap-tour.min.css b/circle/dashboard/static/dashboard/bootstrap-tour.min.css deleted file mode 100644 index 746e4c7..0000000 --- a/circle/dashboard/static/dashboard/bootstrap-tour.min.css +++ /dev/null @@ -1,19 +0,0 @@ -/* =========================================================== -# bootstrap-tour - v0.9.1 -# http://bootstraptour.com -# ============================================================== -# Copyright 2012-2013 Ulrich Sossou -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -*/ -.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8}.tour-step-backdrop{position:relative;z-index:1101;background:inherit}.tour-step-background{position:absolute;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1100}.popover[class*=tour-] .popover-navigation{padding:9px 14px}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none} \ No newline at end of file diff --git a/circle/dashboard/static/dashboard/bootstrap-tour.min.js b/circle/dashboard/static/dashboard/bootstrap-tour.min.js deleted file mode 100644 index dbf5f12..0000000 --- a/circle/dashboard/static/dashboard/bootstrap-tour.min.js +++ /dev/null @@ -1,19 +0,0 @@ -/* =========================================================== -# bootstrap-tour - v0.9.1 -# http://bootstraptour.com -# ============================================================== -# Copyright 2012-2013 Ulrich Sossou -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -*/ -!function(a,b){var c,d;return d=b.document,c=function(){function c(c){this._options=a.extend({name:"tour",steps:[],container:"body",keyboard:!0,storage:b.localStorage,debug:!1,backdrop:!1,redirect:!0,orphan:!1,duration:!1,basePath:"",template:"<div class='popover'> <div class='arrow'></div> <h3 class='popover-title'></h3> <div class='popover-content'></div> <div class='popover-navigation'> <div class='btn-group'> <button class='btn btn-sm btn-default' data-role='prev'>« Prev</button> <button class='btn btn-sm btn-default' data-role='next'>Next »</button> <button class='btn btn-sm btn-default' data-role='pause-resume' data-pause-text='Pause' data-resume-text='Resume'>Pause</button> </div> <button class='btn btn-sm btn-default' data-role='end'>End tour</button> </div> </div>",afterSetState:function(){},afterGetState:function(){},afterRemoveState:function(){},onStart:function(){},onEnd:function(){},onShow:function(){},onShown:function(){},onHide:function(){},onHidden:function(){},onNext:function(){},onPrev:function(){},onPause:function(){},onResume:function(){}},c),this._force=!1,this._inited=!1,this.backdrop={overlay:null,$element:null,$background:null,backgroundShown:!1,overlayElementShown:!1}}return c.prototype.addSteps=function(a){var b,c,d;for(c=0,d=a.length;d>c;c++)b=a[c],this.addStep(b);return this},c.prototype.addStep=function(a){return this._options.steps.push(a),this},c.prototype.getStep=function(b){return null!=this._options.steps[b]?a.extend({id:"step-"+b,path:"",placement:"right",title:"",content:"<p></p>",next:b===this._options.steps.length-1?-1:b+1,prev:b-1,animation:!0,container:this._options.container,backdrop:this._options.backdrop,redirect:this._options.redirect,orphan:this._options.orphan,duration:this._options.duration,template:this._options.template,onShow:this._options.onShow,onShown:this._options.onShown,onHide:this._options.onHide,onHidden:this._options.onHidden,onNext:this._options.onNext,onPrev:this._options.onPrev,onPause:this._options.onPause,onResume:this._options.onResume},this._options.steps[b]):void 0},c.prototype.init=function(a){return this._force=a,this.ended()?(this._debug("Tour ended, init prevented."),this):(this.setCurrentStep(),this._initMouseNavigation(),this._initKeyboardNavigation(),this._onResize(function(a){return function(){return a.showStep(a._current)}}(this)),null!==this._current&&this.showStep(this._current),this._inited=!0,this)},c.prototype.start=function(a){var b;return null==a&&(a=!1),this._inited||this.init(a),null===this._current&&(b=this._makePromise(null!=this._options.onStart?this._options.onStart(this):void 0),this._callOnPromiseDone(b,this.showStep,0)),this},c.prototype.next=function(){var a;return a=this.hideStep(this._current),this._callOnPromiseDone(a,this._showNextStep)},c.prototype.prev=function(){var a;return a=this.hideStep(this._current),this._callOnPromiseDone(a,this._showPrevStep)},c.prototype.goTo=function(a){var b;return b=this.hideStep(this._current),this._callOnPromiseDone(b,this.showStep,a)},c.prototype.end=function(){var c,e;return c=function(c){return function(){return a(d).off("click.tour-"+c._options.name),a(d).off("keyup.tour-"+c._options.name),a(b).off("resize.tour-"+c._options.name),c._setState("end","yes"),c._inited=!1,c._force=!1,c._clearTimer(),null!=c._options.onEnd?c._options.onEnd(c):void 0}}(this),e=this.hideStep(this._current),this._callOnPromiseDone(e,c)},c.prototype.ended=function(){return!this._force&&!!this._getState("end")},c.prototype.restart=function(){return this._removeState("current_step"),this._removeState("end"),this.setCurrentStep(0),this.start()},c.prototype.pause=function(){var a;return a=this.getStep(this._current),a&&a.duration?(this._paused=!0,this._duration-=(new Date).getTime()-this._start,b.clearTimeout(this._timer),this._debug("Paused/Stopped step "+(this._current+1)+" timer ("+this._duration+" remaining)."),null!=a.onPause?a.onPause(this,this._duration):void 0):this},c.prototype.resume=function(){var a;return a=this.getStep(this._current),a&&a.duration?(this._paused=!1,this._start=(new Date).getTime(),this._duration=this._duration||a.duration,this._timer=b.setTimeout(function(a){return function(){return a._isLast()?a.next():a.end()}}(this),this._duration),this._debug("Started step "+(this._current+1)+" timer with duration "+this._duration),null!=a.onResume&&this._duration!==a.duration?a.onResume(this,this._duration):void 0):this},c.prototype.hideStep=function(b){var c,d,e;return(e=this.getStep(b))?(this._clearTimer(),d=this._makePromise(null!=e.onHide?e.onHide(this,b):void 0),c=function(c){return function(){var d;return d=a(e.element),d.data("bs.popover")||d.data("popover")||(d=a("body")),d.popover("destroy").removeClass("tour-"+c._options.name+"-element tour-"+c._options.name+"-"+b+"-element"),e.reflex&&d.css("cursor","").off("click.tour-"+c._options.name),e.backdrop&&c._hideBackdrop(),null!=e.onHidden?e.onHidden(c):void 0}}(this),this._callOnPromiseDone(d,c),d):void 0},c.prototype.showStep=function(a){var b,c,e,f;return this.ended()?(this._debug("Tour ended, showStep prevented."),this):(f=this.getStep(a))?(e=a<this._current,b=this._makePromise(null!=f.onShow?f.onShow(this,a):void 0),c=function(b){return function(){var c,g;if(b.setCurrentStep(a),g=function(){switch({}.toString.call(f.path)){case"[object Function]":return f.path();case"[object String]":return this._options.basePath+f.path;default:return f.path}}.call(b),c=[d.location.pathname,d.location.hash].join(""),b._isRedirect(g,c))return void b._redirect(f,g);if(b._isOrphan(f)){if(!f.orphan)return b._debug("Skip the orphan step "+(b._current+1)+". Orphan option is false and the element doesn't exist or is hidden."),void(e?b._showPrevStep():b._showNextStep());b._debug("Show the orphan step "+(b._current+1)+". Orphans option is true.")}return f.backdrop&&b._showBackdrop(b._isOrphan(f)?void 0:f.element),b._scrollIntoView(f.element,function(){return null!=f.element&&f.backdrop&&b._showOverlayElement(f.element),b._showPopover(f,a),null!=f.onShown&&f.onShown(b),b._debug("Step "+(b._current+1)+" of "+b._options.steps.length)}),f.duration?b.resume():void 0}}(this),this._callOnPromiseDone(b,c),b):void 0},c.prototype.getCurrentStep=function(){return this._current},c.prototype.setCurrentStep=function(a){return null!=a?(this._current=a,this._setState("current_step",a)):(this._current=this._getState("current_step"),this._current=null===this._current?null:parseInt(this._current,10)),this},c.prototype._setState=function(a,b){var c,d;if(this._options.storage){d=""+this._options.name+"_"+a;try{this._options.storage.setItem(d,b)}catch(e){c=e,c.code===DOMException.QUOTA_EXCEEDED_ERR&&this.debug("LocalStorage quota exceeded. State storage failed.")}return this._options.afterSetState(d,b)}return null==this._state&&(this._state={}),this._state[a]=b},c.prototype._removeState=function(a){var b;return this._options.storage?(b=""+this._options.name+"_"+a,this._options.storage.removeItem(b),this._options.afterRemoveState(b)):null!=this._state?delete this._state[a]:void 0},c.prototype._getState=function(a){var b,c;return this._options.storage?(b=""+this._options.name+"_"+a,c=this._options.storage.getItem(b)):null!=this._state&&(c=this._state[a]),(void 0===c||"null"===c)&&(c=null),this._options.afterGetState(a,c),c},c.prototype._showNextStep=function(){var a,b,c;return c=this.getStep(this._current),b=function(a){return function(){return a.showStep(c.next)}}(this),a=this._makePromise(null!=c.onNext?c.onNext(this):void 0),this._callOnPromiseDone(a,b)},c.prototype._showPrevStep=function(){var a,b,c;return c=this.getStep(this._current),b=function(a){return function(){return a.showStep(c.prev)}}(this),a=this._makePromise(null!=c.onPrev?c.onPrev(this):void 0),this._callOnPromiseDone(a,b)},c.prototype._debug=function(a){return this._options.debug?b.console.log("Bootstrap Tour '"+this._options.name+"' | "+a):void 0},c.prototype._isRedirect=function(a,b){return null!=a&&""!==a&&("[object RegExp]"===toString.call(a)&&!a.test(b)||"[object String]"===toString.call(a)&&a.replace(/\?.*$/,"").replace(/\/?$/,"")!==b.replace(/\/?$/,""))},c.prototype._redirect=function(b,c){return a.isFunction(b.redirect)?b.redirect.call(this,c):b.redirect===!0?(this._debug("Redirect to "+c),d.location.href=c):void 0},c.prototype._isOrphan=function(b){return null==b.element||!a(b.element).length||a(b.element).is(":hidden")&&"http://www.w3.org/2000/svg"!==a(b.element)[0].namespaceURI},c.prototype._isLast=function(){return this._current<this._options.steps.length-1},c.prototype._showPopover=function(b,c){var d,e,f,g,h,i;return i=a.extend({},this._options),f=a(a.isFunction(b.template)?b.template(c,b):b.template),e=f.find(".popover-navigation"),h=this._isOrphan(b),h&&(b.element="body",b.placement="top",f=f.addClass("orphan")),d=a(b.element),f.addClass("tour-"+this._options.name+" tour-"+this._options.name+"-"+c),d.addClass("tour-"+this._options.name+"-element tour-"+this._options.name+"-"+c+"-element"),b.options&&a.extend(i,b.options),b.reflex&&d.css("cursor","pointer").on("click.tour-"+this._options.name,function(a){return function(){return a._isLast()?a.next():a.end()}}(this)),b.prev<0&&e.find("[data-role='prev']").addClass("disabled"),b.next<0&&e.find("[data-role='next']").addClass("disabled"),b.duration||e.find("[data-role='pause-resume']").remove(),b.template=f.clone().wrap("<div>").parent().html(),d.popover({placement:b.placement,trigger:"manual",title:b.title,content:b.content,html:!0,animation:b.animation,container:b.container,template:b.template,selector:b.element}).popover("show"),g=d.data("bs.popover")?d.data("bs.popover").tip():d.data("popover").tip(),g.attr("id",b.id),this._reposition(g,b),h?this._center(g):void 0},c.prototype._reposition=function(b,c){var e,f,g,h,i,j,k;if(h=b[0].offsetWidth,f=b[0].offsetHeight,k=b.offset(),i=k.left,j=k.top,e=a(d).outerHeight()-k.top-b.outerHeight(),0>e&&(k.top=k.top+e),g=a("html").outerWidth()-k.left-b.outerWidth(),0>g&&(k.left=k.left+g),k.top<0&&(k.top=0),k.left<0&&(k.left=0),b.offset(k),"bottom"===c.placement||"top"===c.placement){if(i!==k.left)return this._replaceArrow(b,2*(k.left-i),h,"left")}else if(j!==k.top)return this._replaceArrow(b,2*(k.top-j),f,"top")},c.prototype._center=function(c){return c.css("top",a(b).outerHeight()/2-c.outerHeight()/2)},c.prototype._replaceArrow=function(a,b,c,d){return a.find(".arrow").css(d,b?50*(1-b/c)+"%":"")},c.prototype._scrollIntoView=function(c,d){var e,f,g,h,i,j;return e=a(c),e.length?(f=a(b),h=e.offset().top,j=f.height(),i=Math.max(0,h-j/2),this._debug("Scroll into view. ScrollTop: "+i+". Element offset: "+h+". Window height: "+j+"."),g=0,a("body,html").stop(!0,!0).animate({scrollTop:Math.ceil(i)},function(a){return function(){return 2===++g?(d(),a._debug("Scroll into view. Animation end element offset: "+e.offset().top+". Window height: "+f.height()+".")):void 0}}(this))):d()},c.prototype._onResize=function(c,d){return a(b).on("resize.tour-"+this._options.name,function(){return clearTimeout(d),d=setTimeout(c,100)})},c.prototype._initMouseNavigation=function(){var b;return b=this,a(d).off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']:not(.disabled)").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']:not(.disabled)").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']").on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']:not(.disabled)",function(a){return function(b){return b.preventDefault(),a.next()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']:not(.disabled)",function(a){return function(b){return b.preventDefault(),a.prev()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']",function(a){return function(b){return b.preventDefault(),a.end()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']",function(c){var d;return c.preventDefault(),d=a(this),d.text(d.data(b._paused?"pause-text":"resume-text")),b._paused?b.resume():b.pause()})},c.prototype._initKeyboardNavigation=function(){return this._options.keyboard?a(d).on("keyup.tour-"+this._options.name,function(a){return function(b){if(b.which)switch(b.which){case 39:return b.preventDefault(),a._isLast()?a.next():a.end();case 37:if(b.preventDefault(),a._current>0)return a.prev();break;case 27:return b.preventDefault(),a.end()}}}(this)):void 0},c.prototype._makePromise=function(b){return b&&a.isFunction(b.then)?b:null},c.prototype._callOnPromiseDone=function(a,b,c){return a?a.then(function(a){return function(){return b.call(a,c)}}(this)):b.call(this,c)},c.prototype._showBackdrop=function(){return this.backdrop.backgroundShown?void 0:(this.backdrop=a("<div/>",{"class":"tour-backdrop"}),this.backdrop.backgroundShown=!0,a("body").append(this.backdrop))},c.prototype._hideBackdrop=function(){return this._hideOverlayElement(),this._hideBackground()},c.prototype._hideBackground=function(){return this.backdrop.remove(),this.backdrop.overlay=null,this.backdrop.backgroundShown=!1},c.prototype._showOverlayElement=function(b){var c,d,e;return d=a(b),d&&0!==d.length&&!this.backdrop.overlayElementShown?(this.backdrop.overlayElementShown=!0,c=a("<div/>"),e=d.offset(),e.top=e.top,e.left=e.left,c.width(d.innerWidth()).height(d.innerHeight()).addClass("tour-step-background").offset(e),d.addClass("tour-step-backdrop"),a("body").append(c),this.backdrop.$element=d,this.backdrop.$background=c):void 0},c.prototype._hideOverlayElement=function(){return this.backdrop.overlayElementShown?(this.backdrop.$element.removeClass("tour-step-backdrop"),this.backdrop.$background.remove(),this.backdrop.$element=null,this.backdrop.$background=null,this.backdrop.overlayElementShown=!1):void 0},c.prototype._clearTimer=function(){return b.clearTimeout(this._timer),this._timer=null,this._duration=null},c}(),b.Tour=c}(jQuery,window); \ No newline at end of file diff --git a/circle/dashboard/static/dashboard/dashboard.css b/circle/dashboard/static/dashboard/dashboard.css index c0a12ed..1e00494 100644 --- a/circle/dashboard/static/dashboard/dashboard.css +++ b/circle/dashboard/static/dashboard/dashboard.css @@ -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 { @@ -688,7 +688,7 @@ textarea[name="new_members"] { .dashboard-vm-details-connect-command { /* for mobile view */ - margin-bottom: 20px; + padding-bottom: 20px; } #store-list-list { @@ -961,6 +961,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; } @@ -974,6 +982,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; } @@ -1016,3 +1042,12 @@ textarea[name="new_members"] { #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/dashboard.js b/circle/dashboard/static/dashboard/dashboard.js index 3f1d22d..cd7ccfa 100644 --- a/circle/dashboard/static/dashboard/dashboard.js +++ b/circle/dashboard/static/dashboard/dashboard.js @@ -50,6 +50,21 @@ $(function () { return false; }); + $('.tx-tpl-ownership').click(function(e) { + $.ajax({ + type: 'GET', + url: $('.tx-tpl-ownership').attr('href'), + success: function(data) { + $('body').append(data); + $('#confirmation-modal').modal('show'); + $('#confirmation-modal').on('hidden.bs.modal', function() { + $('#confirmation-modal').remove(); + }); + } + }); + return false; + }); + $('.template-choose').click(function(e) { $.ajax({ type: 'GET', @@ -117,7 +132,7 @@ $(function () { $('.js-hidden').hide(); /* favourite star */ - $("#dashboard-vm-list").on('click', '.dashboard-vm-favourite', function(e) { + $("#dashboard-vm-list, .page-header").on('click', '.dashboard-vm-favourite', function(e) { var star = $(this).children("i"); var pk = $(this).data("vm"); if(star.hasClass("fa-star-o")) { @@ -428,7 +443,7 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item' + (is_last ? ' list-group-item-last' : '') + '">' + '<span class="index-vm-list-name">' + - '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name + + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) + '</span>' + '<small class="text-muted"> ' + host + '</small>' + '<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' + @@ -441,14 +456,14 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateGroupHTML(url, name, is_last) { return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+ - '<i class="fa fa-users"></i> '+ name + + '<i class="fa fa-users"></i> '+ safe_tags_replace(name) + '</a>'; } function generateNodeHTML(name, icon, _status, url, is_last) { return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' + '<span class="index-node-list-name">' + - '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name + + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) + '</span>' + '<div style="clear: both;"></div>' + '</a>'; @@ -456,7 +471,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) { function generateNodeTagHTML(name, icon, _status, label , url) { return '<a href="' + url + '" class="label ' + label + '" >' + - '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name + + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) + '</a> '; } @@ -678,3 +693,18 @@ function getParameterByName(name) { results = regex.exec(location.search); return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); } + + +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/disk-list.js b/circle/dashboard/static/dashboard/disk-list.js deleted file mode 100644 index 4796106..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 11b696e..664619f 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 d5cb480..d5159e5 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.length)for(var c=a.length-1;0<=c;c--)a[c].className=a[c].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");window.removeEventListener?window.removeEventListener("keydown",this._onKeyDown,!0):document.detachEvent&&document.detachEvent("onkeydown",this._onKeyDown); +this._currentStep=void 0}}function B(a,b,c,d){var e="";b.style.top=null;b.style.right=null;b.style.bottom=null;b.style.left=null;b.style.marginLeft=null;b.style.marginTop=null;c.style.display="inherit";"undefined"!=typeof d&&null!=d&&(d.style.top=null,d.style.left=null);if(this._introItems[this._currentStep])switch(e=this._introItems[this._currentStep],e="string"===typeof e.tooltipClass?e.tooltipClass:this._options.tooltipClass,b.className=("introjs-tooltip "+e).replace(/^\s+|\s+$/g,""),currentTooltipPosition= +this._introItems[this._currentStep].position,currentTooltipPosition){case "top":b.style.left="15px";b.style.top="-"+(h(b).height+10)+"px";c.className="introjs-arrow bottom";break;case "right":b.style.left=h(a).width+20+"px";c.className="introjs-arrow left";break;case "left":!0==this._options.showStepNumbers&&(b.style.top="15px");b.style.right=h(a).width+20+"px";c.className="introjs-arrow right";break;case "floating":c.style.display="none";a=h(b);b.style.left="50%";b.style.top="50%";b.style.marginLeft= +"-"+a.width/2+"px";b.style.marginTop="-"+a.height/2+"px";"undefined"!=typeof d&&null!=d&&(d.style.left="-"+(a.width/2+18)+"px",d.style.top="-"+(a.height/2+18)+"px");break;case "bottom-right-aligned":c.className="introjs-arrow top-right";b.style.right="0px";b.style.bottom="-"+(h(b).height+10)+"px";break;case "bottom-middle-aligned":d=h(a);a=h(b);c.className="introjs-arrow top-middle";b.style.left=d.width/2-a.width/2+"px";b.style.bottom="-"+(a.height+10)+"px";break;default:b.style.bottom="-"+(h(b).height+ +10)+"px",c.className="introjs-arrow top"}}function v(a){if(a&&this._introItems[this._currentStep]){var b=this._introItems[this._currentStep],c=h(b.element),d=10;"floating"==b.position&&(d=0);a.setAttribute("style","width: "+(c.width+d)+"px; height:"+(c.height+d)+"px; top:"+(c.top-5)+"px;left: "+(c.left-5)+"px;")}}function A(a){var b;"undefined"!==typeof this._introChangeCallback&&this._introChangeCallback.call(this,a.element);var c=this,d=document.querySelector(".introjs-helperLayer");h(a.element); +if(null!=d){var e=d.querySelector(".introjs-helperNumberLayer"),C=d.querySelector(".introjs-tooltiptext"),g=d.querySelector(".introjs-arrow"),y=d.querySelector(".introjs-tooltip"),k=d.querySelector(".introjs-skipbutton"),n=d.querySelector(".introjs-prevbutton"),l=d.querySelector(".introjs-nextbutton");y.style.opacity=0;if(null!=e&&(b=this._introItems[0<=a.step-2?a.step-2:0],null!=b&&"forward"==this._direction&&"floating"==b.position||"backward"==this._direction&&"floating"==a.position))e.style.opacity= +0;v.call(c,d);var m=document.querySelectorAll(".introjs-fixParent");if(m&&0<m.length)for(b=m.length-1;0<=b;b--)m[b].className=m[b].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");b=document.querySelector(".introjs-showElement");b.className=b.className.replace(/introjs-[a-zA-Z]+/g,"").replace(/^\s+|\s+$/g,"");c._lastShowElementTimer&&clearTimeout(c._lastShowElementTimer);c._lastShowElementTimer=setTimeout(function(){null!=e&&(e.innerHTML=a.step);C.innerHTML=a.intro;B.call(c,a.element, +y,g,e);d.querySelector(".introjs-bullets li > 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;b<u;b++){var r=document.createElement("li"),q=document.createElement("a");q.onclick=function(){c.goToStep(this.getAttribute("data-stepnumber"))};0===b&&(q.className="active");q.href="javascript:void(0);";q.innerHTML=" ";q.setAttribute("data-stepnumber",this._introItems[b].step); +r.appendChild(q);p.appendChild(r)}l.appendChild(p);f.className="introjs-tooltipbuttons";!1===this._options.showButtons&&(f.style.display="none");j.className="introjs-tooltip";j.appendChild(n);j.appendChild(l);if(!0==this._options.showStepNumbers){var w=document.createElement("span");w.className="introjs-helperNumberLayer";w.innerHTML=a.step;k.appendChild(w)}j.appendChild(m);k.appendChild(j);l=document.createElement("a");l.onclick=function(){c._introItems.length-1!=c._currentStep&&s.call(c)};l.href= +"javascript:void(0);";l.innerHTML=this._options.nextLabel;n=document.createElement("a");n.onclick=function(){0!=c._currentStep&&x.call(c)};n.href="javascript:void(0);";n.innerHTML=this._options.prevLabel;k=document.createElement("a");k.className="introjs-button introjs-skipbutton";k.href="javascript:void(0);";k.innerHTML=this._options.skipLabel;k.onclick=function(){c._introItems.length-1==c._currentStep&&"function"===typeof c._introCompleteCallback&&c._introCompleteCallback.call(c);c._introItems.length- +1!=c._currentStep&&"function"===typeof c._introExitCallback&&c._introExitCallback.call(c);t.call(c,c._targetElement)};f.appendChild(k);1<this._introItems.length&&(f.appendChild(n),f.appendChild(l));j.appendChild(f);B.call(c,a.element,j,m,w)}0==this._currentStep&&1<this._introItems.length?(n.className="introjs-button introjs-prevbutton introjs-disabled",l.className="introjs-button introjs-nextbutton",k.innerHTML=this._options.skipLabel):this._introItems.length-1==this._currentStep||1==this._introItems.length? +(k.innerHTML=this._options.doneLabel,n.className="introjs-button introjs-prevbutton",l.className="introjs-button introjs-nextbutton introjs-disabled"):(n.className="introjs-button introjs-prevbutton",l.className="introjs-button introjs-nextbutton",k.innerHTML=this._options.skipLabel);l.focus();a.element.className+=" introjs-showElement";b=z(a.element,"position");"absolute"!==b&&"relative"!==b&&(a.element.className+=" introjs-relativePosition");for(b=a.element.parentNode;null!=b&&"body"!==b.tagName.toLowerCase();){m= +z(b,"z-index");j=parseFloat(z(b,"opacity"));if(/[0-9]+/.test(m)||1>j)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;e<d;e++){var f=r(this._options.steps[e]);f.step=b.length+1;"string"===typeof f.element&&(f.element=document.querySelector(f.element));if("undefined"===typeof f.element||null==f.element){var g=document.querySelector(".introjsFloatingElement");null==g&&(g=document.createElement("div"),g.className= +"introjsFloatingElement",document.body.appendChild(g));f.element=g;f.position="floating"}null!=f.element&&b.push(f)}else{d=a.querySelectorAll("*[data-intro]");if(1>d.length)break a;e=0;for(f=d.length;e<f;e++){var g=d[e],h=parseInt(g.getAttribute("data-step"),10);0<h&&(b[h-1]={element:g,intro:g.getAttribute("data-intro"),step:parseInt(g.getAttribute("data-step"),10),tooltipClass:g.getAttribute("data-tooltipClass"),position:g.getAttribute("data-position")||this._options.tooltipPosition})}e=h=0;for(f= +d.length;e<f;e++)if(g=d[e],null==g.getAttribute("data-step")){for(;"undefined"!=typeof b[h];)h++;b[h]={element:g,intro:g.getAttribute("data-intro"),step:h+1,tooltipClass:g.getAttribute("data-tooltipClass"),position:g.getAttribute("data-position")||this._options.tooltipPosition}}}e=[];for(d=0;d<b.length;d++)b[d]&&e.push(b[d]);b=e;b.sort(function(a,b){return a.step-b.step});c._introItems=b;D.call(c,a)&&(s.call(c),a.querySelector(".introjs-skipbutton"),a.querySelector(".introjs-nextbutton"),c._onKeyDown= +function(b){if(27===b.keyCode&&!0==c._options.exitOnEsc)t.call(c,a),void 0!=c._introExitCallback&&c._introExitCallback.call(c);else if(37===b.keyCode)x.call(c);else if(39===b.keyCode||13===b.keyCode)s.call(c),b.preventDefault?b.preventDefault():b.returnValue=!1},c._onResize=function(){v.call(c,document.querySelector(".introjs-helperLayer"))},window.addEventListener?(this._options.keyboardNavigation&&window.addEventListener("keydown",c._onKeyDown,!0),window.addEventListener("resize",c._onResize,!0)): +document.attachEvent&&(this._options.keyboardNavigation&&document.attachEvent("onkeydown",c._onKeyDown),document.attachEvent("onresize",c._onResize)))}return this},goToStep:function(a){this._currentStep=a-2;"undefined"!==typeof this._introItems&&s.call(this);return this},nextStep:function(){s.call(this);return this},previousStep:function(){x.call(this);return this},exit:function(){t.call(this,this._targetElement)},refresh:function(){v.call(this,document.querySelector(".introjs-helperLayer"));return this}, +onbeforechange:function(a){if("function"===typeof a)this._introBeforeChangeCallback=a;else throw Error("Provided callback for onbeforechange was not a function");return this},onchange:function(a){if("function"===typeof a)this._introChangeCallback=a;else throw Error("Provided callback for onchange was not a function.");return this},onafterchange:function(a){if("function"===typeof a)this._introAfterChangeCallback=a;else throw Error("Provided callback for onafterchange was not a function");return this}, +oncomplete:function(a){if("function"===typeof a)this._introCompleteCallback=a;else throw Error("Provided callback for oncomplete was not a function.");return this},onexit:function(a){if("function"===typeof a)this._introExitCallback=a;else throw Error("Provided callback for onexit was not a function.");return this}};return p.introJs=u}); diff --git a/circle/dashboard/static/dashboard/introjs/introjs.min.css b/circle/dashboard/static/dashboard/introjs/introjs.min.css new file mode 100644 index 0000000..70ed6e6 --- /dev/null +++ b/circle/dashboard/static/dashboard/introjs/introjs.min.css @@ -0,0 +1,12 @@ +.introjs-overlay{position:absolute;z-index:999999;background-color:#000;opacity:0;background:-moz-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-webkit-gradient(radial,center center,0px,center center,100%,color-stop(0%,rgba(0,0,0,0.4)),color-stop(100%,rgba(0,0,0,0.9)));background:-webkit-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-o-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-ms-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#66000000',endColorstr='#e6000000',GradientType=1);-ms-filter:"alpha(opacity=50)";filter:alpha(opacity=50);-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-fixParent{z-index:auto !important;opacity:1.0 !important}.introjs-showElement,tr.introjs-showElement>td,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 15d87e8..832a33c 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 4411422..9fbea33 100644 --- a/circle/dashboard/static/dashboard/node-list.js +++ b/circle/dashboard/static/dashboard/node-list.js @@ -12,40 +12,6 @@ $(function() { 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( - $("<a/>", { - '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(); diff --git a/circle/dashboard/static/dashboard/template-list.js b/circle/dashboard/static/dashboard/template-list.js index e0d82b5..a3e45ff 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 66d6f71..10111e8 100644 --- a/circle/dashboard/static/dashboard/vm-common.js +++ b/circle/dashboard/static/dashboard/vm-common.js @@ -19,7 +19,7 @@ $(function() { $('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary'); } }); - return false; + e.preventDefault(); }); /* if the operation fails show the modal again */ diff --git a/circle/dashboard/static/dashboard/vm-details.js b/circle/dashboard/static/dashboard/vm-details.js index dbaf4b4..e851d86 100644 --- a/circle/dashboard/static/dashboard/vm-details.js +++ b/circle/dashboard/static/dashboard/vm-details.js @@ -28,7 +28,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 +61,7 @@ $(function() { } } }); - return false; + e.preventDefault(); }); /* remove tag */ @@ -205,11 +205,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,7 +236,7 @@ $(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"); @@ -244,7 +244,7 @@ $(function() { ta.val(""); ta.focus(); ta.val(tmp) - return false; + e.preventDefault(); }); /* description update ajax */ @@ -316,6 +316,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,10 +362,7 @@ 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,6 +392,7 @@ function checkNewActivity(runs) { } else { icon.prop("class", "fa " + data['icon']); } + $("#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']) { diff --git a/circle/dashboard/static/dashboard/vm-tour.js b/circle/dashboard/static/dashboard/vm-tour.js index cfffc7c..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") + ' <i class="fa fa-chevron-right"></i>', + 'prevLabel': '<i class="fa fa-chevron-left"></i> ' + gettext("Previous"), + 'skipLabel': '<i class="fa fa-times"></i> ' + 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"); + } -function createTemplateTour() { - var ttour = new Tour({ - storage: false, - name: "template", - template: "<div class='popover'>" + - "<div class='arrow'></div>" + - "<h3 class='popover-title'></h3>" + - "<div class='popover-content'></div>" + - "<div class='popover-navigation'>" + - "<div class='btn-group'>" + - "<button class='btn btn-sm btn-default' data-role='prev'>" + - '<i class="fa fa-chevron-left"></i> ' + gettext("Prev") + "</button> " + - "<button class='btn btn-sm btn-default' data-role='next'>" + - gettext("Next") + ' <i class="fa fa-chevron-right"></i></button> ' + - "<button class='btn btn-sm btn-default' data-role='pause-resume' data-pause-text='Pause' data-resume-text='Resume'>Pause</button> " + - "</div>" + - "<button class='btn btn-sm btn-default' data-role='end'>" + - gettext("End tour") + ' <i class="fa fa-flag-checkered"></i></button>' + - "</div>" + - "</div>", - }); + /* 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(); - ttour.addStep({ - element: "#vm-details-template-tour-button", - title: gettext("Template Tutorial Tour"), - content: "<p>" + gettext("Welcome to the template tutorial. In this quick tour, we gonna show you how to do the steps described above.") + "</p>" + - "<p>" + gettext('For the next tour step press the "Next" button or the right arrow (or "Back" button/left arrow for the previous step).') + "</p>" + - "<p>" + 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!") + "</p>", - placement: "bottom", - backdrop: true, + return false; }); - 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"); - }, + $(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: '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"); - }, - }); - ttour.addStep({ - element: '#vm-details-resources-form', - placement: 'top', - backdrop: true, - title: gettext("Resources"), - content: '<p><strong>' + gettext("CPU priority") + ":</strong> " + gettext("higher is better") + "</p>" + - '<p><strong>' + gettext("CPU count") + ":</strong> " + gettext("number of CPU cores.") + "</p>" + - '<p><strong>' + gettext("RAM amount") + ":</strong> " + gettext("amount of RAM.") + "</p>", - onShow: function() { - $('a[href="#resources"]').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"]'; + } - 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"); + steps = [ + { + element: document.querySelector("#vm-details-start-template-tour"), + intro: "<p>" + gettext("Welcome to the template tutorial. In this quick tour, we are going to show you how to do the steps described above.") + "</p>" + + "<p>" + gettext('For the next tour step press the "Next" button or the right arrow (or "Back" button/left arrow for the previous step).') + "</p>" }, - }); - - 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="#home"]'), + intro: gettext("In this tab you can extend the expiration date of your virtual machine, add tags and modify the name and description."), }, - }); - + { + 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."), + }, + { + 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."), + }, + { + 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."), + } + ]; - ttour.addStep({ - element: "#ops", - title: '<i class="fa fa-play"></i> ' + gettext("Deploy"), - placement: "left", - backdrop: true, - content: gettext("Deploy the virtual machine."), - }); + if($("#vm-details-resources-save").length) { + steps.push( + { + element: document.querySelector('#vm-details-resources-form'), + intro: '<p><strong>' + gettext("CPU priority") + ":</strong> " + + gettext("higher is better") + "</p>" + + "<p><strong>" + gettext("CPU count") + ":</strong> " + + gettext("number of CPU cores.") + "</p>" + + "<p><strong>" + gettext("RAM amount") + ":</strong> " + + gettext("amount of RAM.") + "</p>", + position: "top", + } + ); + } - 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!"), - - }); + 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", + } + ); + } - 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: '<i class="fa fa-floppy-o"></i> ' + 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; + steps.push( + { + element: document.querySelector('a[href="#network"]'), + intro: gettext('You can add new network interfaces or remove existing ones here.'), + }, + { + 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/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/dashboard/_disk-list-element.html b/circle/dashboard/templates/dashboard/_disk-list-element.html index 23ae790..3cbc2e1 100644 --- a/circle/dashboard/templates/dashboard/_disk-list-element.html +++ b/circle/dashboard/templates/dashboard/_disk-list-element.html @@ -8,7 +8,7 @@ <span class="operation-wrapper"> <a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}" class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn - {% if op.resize_disk.disabled %}disabled{% endif %}"> + {% if op.remove_disk.disabled %}disabled{% endif %}"> <i class="fa fa-{{ op.remove_disk.icon }}"></i> {% trans "Remove" %} </a> </span> diff --git a/circle/dashboard/templates/dashboard/_vm-remove-port.html b/circle/dashboard/templates/dashboard/_vm-remove-port.html new file mode 100644 index 0000000..44fe133 --- /dev/null +++ b/circle/dashboard/templates/dashboard/_vm-remove-port.html @@ -0,0 +1,23 @@ +{% extends "dashboard/operate.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block formfields %} + {% if form %} + {% crispy form %} + {% endif %} + + {% if form.fields.rule.initial != None %} + {% with rule=form.fields.rule.initial %} + <dl> + <dt>{% trans "Port" %}:</dt> + <dd>{{ rule.dport }}/{{ rule.proto }}</dd> + <dt>{% trans "Host" %}:</dt> + <dd>{{ rule.host.hostname }}</dd> + <dt>{% trans "Vlan" %}:</dt> + <dd>{{ rule.host.vlan.name }}</dd> + </dl> + {% endwith %} + {% endif %} + +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/base.html b/circle/dashboard/templates/dashboard/base.html index 3bc73bb..7a1ebcc 100644 --- a/circle/dashboard/templates/dashboard/base.html +++ b/circle/dashboard/templates/dashboard/base.html @@ -6,7 +6,7 @@ {% block extra_link %} <link rel="stylesheet" href="{{ STATIC_URL }}dashboard/loopj-jquery-simple-slider/css/simple-slider.css"/> - <link href="{{ STATIC_URL }}dashboard/bootstrap-tour.min.css" rel="stylesheet"> + <link rel="stylesheet" href="{{ STATIC_URL }}dashboard/introjs/introjs.min.css"> <link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet"> {% 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 @@ <form action="{% url "dashboard.views.status-node" pk=object.pk %}" method="POST"> {% csrf_token %} <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button> - <input type="hidden" name="change_status" value=""/> + <input type="hidden" name="change_status" value=""/> <button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button> </form> </div> - <div class="clearfix"></div> + <div class="clearfix"></div> </div> </div><!-- /.modal-content --> 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 <strong>{{ member }}</strong> from <strong>{{ object }}</strong>? + Are you sure you want to remove <strong>{{ member }}</strong> from <strong>{{ object }}</strong>? {%endblocktrans%} {% endif %} <br /> 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 @@ <div class="pull-right"> <form action="" method="POST"> {% csrf_token %} - <a class="btn btn-default">{% trans "Back" %}</a> - <input type="hidden" name="flush" value=""/> - <button class="btn btn-warning">{% trans "Yes" %}</button> + <a class="btn btn-default">{% trans "Back" %}</a> + <input type="hidden" name="flush" value=""/> + <button class="btn btn-warning">{% trans "Yes" %}</button> </form> </div> </div> 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 %} <a class="btn btn-default">{% trans "Cancel" %}</a> <button type="button" class="btn btn-default" data-dismiss="modal"></button> - <input type="hidden" name="change_status" value=""/> + <input type="hidden" name="change_status" value=""/> <button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button> </form> </div> diff --git a/circle/dashboard/templates/dashboard/confirm/base-transfer-ownership.html b/circle/dashboard/templates/dashboard/confirm/transfer-instance-ownership.html similarity index 100% rename from circle/dashboard/templates/dashboard/confirm/base-transfer-ownership.html rename to circle/dashboard/templates/dashboard/confirm/transfer-instance-ownership.html index 2182c63..217ea5f 100644 --- a/circle/dashboard/templates/dashboard/confirm/base-transfer-ownership.html +++ b/circle/dashboard/templates/dashboard/confirm/transfer-instance-ownership.html @@ -6,15 +6,15 @@ <div class="panel panel-default"> <div class="panel-heading"> <h3 class="no-margin"> - {% trans "Ownership transfer" %} + {% trans "Ownership transfer" %} </h3> </div> <div class="panel-body"> - {% blocktrans with owner=instance.owner name=instance.name id=instance.id%} - <strong>{{ owner }}</strong> offered to take the ownership of - virtual machine <strong>{{name}} ({{id}})</strong>. - Do you accept the responsility of being the host's owner? - {% endblocktrans %} + {% blocktrans with owner=instance.owner name=instance.name id=instance.id%} + <strong>{{ owner }}</strong> offered to take the ownership of + virtual machine <strong>{{name}} ({{id}})</strong>. + Do you accept the responsility of being the host's owner? + {% endblocktrans %} <div class="pull-right"> <form action="" method="POST"> {% csrf_token %} diff --git a/circle/dashboard/templates/dashboard/confirm/transfer-template-ownership.html b/circle/dashboard/templates/dashboard/confirm/transfer-template-ownership.html new file mode 100644 index 0000000..077202c --- /dev/null +++ b/circle/dashboard/templates/dashboard/confirm/transfer-template-ownership.html @@ -0,0 +1,28 @@ +{% extends "dashboard/base.html" %} +{% load i18n %} + +{% block content %} + <div class="body-content"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="no-margin"> + {% trans "Ownership transfer" %} + </h3> + </div> + <div class="panel-body"> + {% blocktrans with owner=instance.owner name=instance.name id=instance.id%} + <strong>{{ owner }}</strong> offered to take the ownership of + template <strong>{{name}} ({{id}})</strong>. + Do you accept the responsility of being the template's owner? + {% endblocktrans %} + <div class="pull-right"> + <form action="" method="POST"> + {% csrf_token %} + <a class="btn btn-default" href="{% url "dashboard.index" %}">{% trans "No" %}</a> + <input type="hidden" name="key" value="{{ key }}"/> + <button class="btn btn-danger" type="submit">{% trans "Yes" %}</button> + </form> + </div> + </div> + </div> +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/group-detail.html b/circle/dashboard/templates/dashboard/group-detail.html index d18aa86..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 @@ <div class="body-content"> <div class="page-header"> <div class="pull-right" style="padding-top: 15px;"> - <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button"><i class="fa fa-pencil"></i></a> - <a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash-o"></i></a> - <a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a> + <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button"> + <i class="fa fa-pencil"></i> + </a> + <a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"> + <i class="fa fa-trash-o"></i> + </a> + <a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"> + <i class="fa fa-question"></i> + </a> </div> <h1> <div id="group-details-rename"> @@ -39,122 +46,125 @@ </li> </ul> </div> - </div> + </div><!-- .page-header --> <div class="row"> <div class="col-md-12" id="group-detail-pane"> - <div class="panel panel-default" id="group-detail-panel"> - <div class="tab-content panel-body" id="group-form-body"> - - <form method="POST" action="{% url "dashboard.views.group-update" pk=group.pk %}"> -{% csrf_token %} -{% crispy group_profile_form %} -</form> - -<hr /> - -<h3>{% trans "Available objects for this group" %}</h3> -<ul class="dashboard-profile-vm-list fa-ul"> - {% for i in vm_objects %} - <li> - <a href="{{ i.get_absolute_url }}"> - <i class="fa fa-li {{ i.get_status_icon }}"></i> - {{ i }} - </a> - </li> - {% endfor %} - {% for t in template_objects %} - <li> - <a href="{{ t.get_absolute_url }}"> - <i class="fa fa-li fa-puzzle-piece"></i> - {{ t }} - </a> - </li> - {% endfor %} - {% for g in group_objects %} - <li> - <a href="{{ g.get_absolute_url }}"> - <i class="fa fa-li fa-users"></i> - {{ g }} - </a> - </li> - {% endfor %} -</ul> - -<hr /> + <div class="panel panel-default panel-body" id="group-detail-panel"> + <form method="POST" action="{% url "dashboard.views.group-update" pk=group.pk %}"> + {% csrf_token %} + {% crispy group_profile_form %} + </form> + <hr /> -<h3>{% trans "User list"|capfirst %} - {% if perms.auth.add_user %} - <a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a> - {% endif %} -</h3> -<form action="" method="post">{% csrf_token %} -<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table"> - <tbody> - <thead><tr><th></th><th>{% trans "Who" %}</th><th>{% trans "Remove" %}</th></tr></thead> - {% for i in users %} - <tr> - <td> - <i class="fa fa-user"></i> - </td> - <td> - <a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}" - >{% include "dashboard/_display-name.html" with user=i show_org=True %}</a> - </td> - <td> - <a data-group_pk="{{ group.pk }}" data-member_pk="{{i.pk}}" href="{% url "dashboard.views.remove-user" member_pk=i.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times"><span class="sr-only">{% trans "remove" %}</span></i></a> - </td> - </tr> - {% endfor %} - {% for i in future_users %} - <tr> - <td> - <i class="fa fa-user text-muted"></i> - </td> - <td> {{ i.org_id }} </td> - <td> - <a href="{% url "dashboard.views.remove-future-user" member_org_id=i.org_id group_pk=group.pk %}" - class="real-link btn-link btn-xs"> - <i class="fa fa-times"><span class="sr-only">{% trans "remove" %}</span></i></a> - </td> - </tr> - {% endfor %} - <tr> - <td><i class="fa fa-plus"></i></td> - <td colspan="2"> - {{addmemberform.new_member}} - </td> - </tr> - </tbody> - </table> - <textarea name="new_members" class="form-control" - placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea> - <div class="form-actions"> - <button type="submit" class="btn btn-success">{% trans "Save" %}</button> - </div> - </form> + <h3>{% trans "Available objects for this group" %}</h3> + <ul class="dashboard-profile-vm-list fa-ul"> + {% for i in vm_objects %} + <li> + <a href="{{ i.get_absolute_url }}"> + <i class="fa fa-li {{ i.get_status_icon }}"></i> + {{ i }} + </a> + </li> + {% endfor %} + {% for t in template_objects %} + <li> + <a href="{{ t.get_absolute_url }}"> + <i class="fa fa-li fa-puzzle-piece"></i> + {{ t }} + </a> + </li> + {% endfor %} + {% for g in group_objects %} + <li> + <a href="{{ g.get_absolute_url }}"> + <i class="fa fa-li fa-users"></i> + {{ g }} + </a> + </li> + {% endfor %} + </ul> + <hr /> - <hr /> -<h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3> -{% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %} -{% if user.is_superuser %} - <hr /> + <h3> + {% trans "User list" %} + {% if perms.auth.add_user %} + <a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right"> + {% trans "Create user" %} + </a> + {% endif %} + </h3> + <form action="" method="post">{% csrf_token %} + <table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table"> + <tbody> + <thead><tr><th></th><th>{% trans "Who" %}</th><th>{% trans "Remove" %}</th></tr></thead> + {% for i in users %} + <tr> + <td> + <i class="fa fa-user"></i> + </td> + <td> + <a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}" + >{% include "dashboard/_display-name.html" with user=i show_org=True %}</a> + </td> + <td> + <a data-group_pk="{{ group.pk }}" data-member_pk="{{i.pk}}" href="{% url "dashboard.views.remove-user" member_pk=i.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times"> + <span class="sr-only">{% trans "remove" %}</span></i> + </a> + </td> + </tr> + {% endfor %} + {% for i in future_users %} + <tr> + <td> + <i class="fa fa-user text-muted"></i> + </td> + <td> {{ i.org_id }} </td> + <td> + <a href="{% url "dashboard.views.remove-future-user" member_org_id=i.org_id group_pk=group.pk %}" + class="real-link btn-link btn-xs"> + <i class="fa fa-times"><span class="sr-only">{% trans "remove" %}</span></i></a> + </td> + </tr> + {% endfor %} + <tr> + <td><i class="fa fa-plus"></i></td> + <td colspan="2"> + {{addmemberform.new_member}} + </td> + </tr> + </tbody> + </table> + <textarea name="new_members" class="form-control" + placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea> + <div class="form-actions"> + <button type="submit" class="btn btn-success">{% trans "Save" %}</button> + </div> + </form> + <hr /> + <h3 id="group-detail-perm-header">{% trans "Access permissions" %}</h3> + {% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %} -<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script> -<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script> -{{ group_perm_form.media }} + {% if user.is_superuser %} + <hr /> -<h3>{% trans "Group permissions" %}</h3> + <script type="text/javascript" src="/static/admin/js/jquery.min.js"></script> + <script type="text/javascript" src="/static/admin/js/jquery.init.js"></script> + {{ group_perm_form.media }} -<div id="group-detail-permissions"> -{% crispy group_perm_form %} -</div> + <h3>{% trans "Group permissions" %}</h3> -<link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" /> + <div id="group-detail-permissions"> + {% crispy group_perm_form %} + </div> -{% endif %} - </div> - </div> + <link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" /> + {% endif %} + </div> </div> </div> </div> {% endblock %} + +{% block extra_js %} + <script type="text/javascript" src="{% static "dashboard/group-details.js" %}"></script> +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/group-list.html b/circle/dashboard/templates/dashboard/group-list.html index 92dfaa5..a47f2cd 100644 --- a/circle/dashboard/templates/dashboard/group-list.html +++ b/circle/dashboard/templates/dashboard/group-list.html @@ -10,10 +10,10 @@ <div class="col-md-12"> <div class="panel panel-default"> <div class="panel-heading"> - <h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3> + <h3 class="no-margin"><i class="fa fa-group"></i> {% trans "Groups" %}</h3> </div> <div class="panel-body"> - <div id="table_container"> + <div id="table_container"> <div id="rendered_table" class="panel-body"> {% render_table table %} </div> diff --git a/circle/dashboard/templates/dashboard/index-groups.html b/circle/dashboard/templates/dashboard/index-groups.html index 9f867b3..e1aba82 100644 --- a/circle/dashboard/templates/dashboard/index-groups.html +++ b/circle/dashboard/templates/dashboard/index-groups.html @@ -26,7 +26,7 @@ <div class="col-sm-6 text-right"> <a class="btn btn-primary btn-xs" href="{% url "dashboard.views.group-list" %}"> <i class="fa fa-chevron-circle-right"></i> - {% if more_groups > 0 %} + {% if more_groups > 0 %} {% blocktrans count more=more_groups %} <strong>{{ more }}</strong> more {% plural %} diff --git a/circle/dashboard/templates/dashboard/index-nodes.html b/circle/dashboard/templates/dashboard/index-nodes.html index 0ff215c..8574869 100644 --- a/circle/dashboard/templates/dashboard/index-nodes.html +++ b/circle/dashboard/templates/dashboard/index-nodes.html @@ -74,9 +74,11 @@ {% trans "list" %} {% endif %} </a> + {% if request.user.is_superuser %} <a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}"> <i class="fa fa-plus-circle"></i> {% trans "new" %} </a> + {% endif %} </div> </div> </div> diff --git a/circle/dashboard/templates/dashboard/index.html b/circle/dashboard/templates/dashboard/index.html index 4a7da1b..0662e22 100644 --- a/circle/dashboard/templates/dashboard/index.html +++ b/circle/dashboard/templates/dashboard/index.html @@ -35,7 +35,7 @@ </div> {% endif %} - {% if user.is_superuser %} + {% if perms.vm.view_statistics %} <div class="col-lg-4 col-sm-6"> {% include "dashboard/index-nodes.html" %} </div> diff --git a/circle/dashboard/templates/dashboard/instanceactivity_detail.html b/circle/dashboard/templates/dashboard/instanceactivity_detail.html index 3733550..2784e99 100644 --- a/circle/dashboard/templates/dashboard/instanceactivity_detail.html +++ b/circle/dashboard/templates/dashboard/instanceactivity_detail.html @@ -50,7 +50,7 @@ <dd>{{ object.task_uuid|default:'n/a' }}</dd> <dt>{% trans "status" %}</dt> - <dd>{{ object.get_status_id }}</dd> + <dd id="activity_status">{{ object.get_status_id }}</dd> <dt>{% trans "result" %}</dt> 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 @@ <div class="col-md-12"> <div class="panel panel-default"> <div class="panel-heading"> - <h3 class="no-margin"><i class="fa fa-plus"></i> {% trans "Add Trait" %}</h3> + <h3 class="no-margin"><i class="fa fa-plus"></i> {% trans "Add Trait" %}</h3> </div> <div class="panel-body"> {% with form=form %} diff --git a/circle/dashboard/templates/dashboard/node-detail.html b/circle/dashboard/templates/dashboard/node-detail.html index 1797092..b7ddc48 100644 --- a/circle/dashboard/templates/dashboard/node-detail.html +++ b/circle/dashboard/templates/dashboard/node-detail.html @@ -6,14 +6,16 @@ {% block content %} <div class="body-content"> <div class="page-header"> + {% if request.user.is_superuser %} <div class="pull-right" id="ops"> {% include "dashboard/vm-detail/_operations.html" %} </div> - <div class="pull-right" style="padding-top: 15px;"> - <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a> - <a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a> + <div class="pull-right" style="padding-top: 15px;"> + <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a> + <a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a> </div> - <h1> + {% endif %} + <h1> <div id="node-details-rename"> <form action="" method="POST" id="node-details-rename-form"> {% csrf_token %} @@ -69,26 +71,26 @@ {% trans "Resources" %} </a> </li> - <li> + <li> <a href="{% url "dashboard.views.vm-list" %}?s=node:{{ node.name }}" target="blank" class="text-center"> <i class="fa fa-desktop fa-2x"></i><br> {% trans "Virtual Machines" %} </a> </li> - <li> + <li> <a href="#activity" data-toggle="pill" class="text-center"> <i class="fa fa-clock-o fa-2x"></i><br> {% trans "Activity" %} </a> </li> - </ul> + </ul> <div id="panel-body" class="tab-content panel-body"> <div class="tab-pane active" id="home">{% include "dashboard/node-detail/home.html" %}</div> - <div class="tab-pane" id="resources">{% include "dashboard/node-detail/resources.html" %}</div> + <div class="tab-pane" id="resources">{% include "dashboard/node-detail/resources.html" %}</div> <div class="tab-pane" id="activity">{% include "dashboard/node-detail/activity.html" %}</div> - </div> + </div> </div> </div> </div> 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 %} <div class="label label-success label-tag" style="display: inline-block"> {{ t }} - <a data-trait-pk="{{ t.pk }}" href="#" class="node-details-remove-trait"><i class="fa fa-times"></i></a> + <a data-trait-pk="{{ t.pk }}" href="#" class="node-details-remove-trait"><i class="fa fa-times"></i></a> </div> {% endfor %} {% else %} - <small>{% trans "No trait added!" %}</small> + <small>{% trans "No trait added!" %}</small> {% endif %} </div> {% load crispy_forms_tags %} + <style> .row { margin-bottom: 15px; } </style> - <form action="{% url "dashboard.views.node-addtrait" node.pk %}" method="POST"> - {% csrf_token %} - {% crispy trait_form %} - </form> + {% if request.user.is_superuser %} + <form action="{% url "dashboard.views.node-addtrait" node.pk %}" method="POST"> + {% csrf_token %} + {% crispy trait_form %} + </form> + {% endif %} </div><!-- id:node-details-traits --> </div> <div class="col-md-8"> diff --git a/circle/dashboard/templates/dashboard/node-detail/resources.html b/circle/dashboard/templates/dashboard/node-detail/resources.html index 6a0c288..00c4751 100644 --- a/circle/dashboard/templates/dashboard/node-detail/resources.html +++ b/circle/dashboard/templates/dashboard/node-detail/resources.html @@ -10,6 +10,16 @@ <dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled }}</dd> <dt>{% trans "Host online" %}:</dt><dd> {{ node.online }}</dd> <dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd> + <dt>{% trans "Driver Version:" %}</dt> + <dd> + {% if node.driver_version %} + {{ node.driver_version.branch }} at + {{ node.driver_version.commit }} ({{ node.driver_version.commit_text }}) + {% if node.driver_version.is_dirty %} + <span class="label label-danger">{% trans "with uncommitted changes!" %}</span> + {% endif %} + {% endif %} + </dd> <dt>{% trans "Host owner" %}:</dt> <dd> {% include "dashboard/_display-name.html" with user=node.host.owner show_org=True %} @@ -18,10 +28,12 @@ <dt>{% trans "Host name" %}:</dt> <dd> {{ node.host.hostname }} + {% if request.user.is_superuser %} <a href="{{ node.host.get_absolute_url }}" class="btn btn-default btn-xs"> <i class="fa fa-pencil"></i> {% trans "Edit host" %} </a> + {% endif %} </dd> </dl> 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 @@ <div class="list-group-item"> <div class="row"> <div class="col-sm-6"> - <a href="{% url "dashboard.views.store-upload"%}?directory={{ current }}" + <a href="{% url "dashboard.views.store-upload"%}?directory={{ current|urlencode }}" class="btn btn-info btn-xs js-hidden"> {% trans "Upload" %} </a> - <form action="" data-action="{% url "dashboard.views.store-upload-url" %}" + <form action="" data-action="{% url "dashboard.views.store-upload-url" %}" method="POST" enctype="multipart/form-data" class="no-js-hidden" id="store-upload-form"> {% csrf_token %} - <input type="hidden" name="current_dir" value="{{ current }}"/> + <input type="hidden" name="current_dir" value="{{ current|urlencode }}"/> <input type="hidden" name="next" value="{{ next_url }}"/> <div class="input-group" style="max-width: 350px;"> <span class="input-group-btn" id="store-upload-browse"> <span class="btn btn-primary btn-xs"> {% trans "Browse..." %} - </span> + </span> </span> - <input type="text" class="form-control input-tags" + <input type="text" class="form-control input-tags" id="store-upload-filename"/> <span class="input-group-btn"> <button type="submit" class="btn btn-primary btn-xs" disabled> @@ -33,13 +33,13 @@ </div><!-- .col-sm-6 upload --> <div class="col-sm-6"> - <a href="{% url "dashboard.views.store-remove" %}?path={{ current }}" - class="btn btn-danger btn-xs pull-right store-action-button" + <a href="{% url "dashboard.views.store-remove" %}?path={{ current|urlencode }}" + class="btn btn-danger btn-xs pull-right store-action-button" title="{% trans "Remove directory" %}"> <i class="fa fa-times"></i> </a> - <a href="{% url "dashboard.views.store-download" %}?path={{ current }}" - class="btn btn-primary btn-xs pull-right store-action-button" + <a href="{% url "dashboard.views.store-download" %}?path={{ current|urlencode }}" + class="btn btn-primary btn-xs pull-right store-action-button" title="{% trans "Download directory" %}"> <i class="fa fa-cloud-download"></i> </a> @@ -51,7 +51,7 @@ <span class="input-group-addon input-tags" title="{% trans "New directory" %}"> <i class="fa fa-folder-open"></i> </span> - <input type="text" class="form-control input-tags" name="name" + <input type="text" class="form-control input-tags" name="name" placeholder="{% trans "Name "%}" required/> <span class="input-group-btn"> <input type="submit" class="btn btn-success btn-xs" value="{% trans "Create" %}"/> @@ -64,7 +64,7 @@ </div><!-- .list-group --> <div class="list-group" id="store-list-list"> - <a href="{% url "dashboard.views.store-list" %}?directory={{ up_url }}" + <a href="{% url "dashboard.views.store-list" %}?directory={{ up_url|urlencode }}" class="list-group-item store-list-item" data-item-type="D"> {% if current == "/" %} <div class="store-list-item-icon"> @@ -85,8 +85,8 @@ {% for f in root %} <a class="list-group-item store-list-item" data-item-type="{{ f.TYPE }}" - href="{% if f.TYPE == "D" %}{% url "dashboard.views.store-list" %}?directory={{ f.path }}{% else %} - {% url "dashboard.views.store-download" %}?path={{ f.path }}{% endif %}" + href="{% if f.TYPE == "D" %}{% url "dashboard.views.store-list" %}?directory={{ f.path|urlencode }}{% else %} + {% url "dashboard.views.store-download" %}?path={{ f.path|urlencode }}{% endif %}" > <div class="store-list-item-icon"> <i class=" @@ -101,7 +101,7 @@ <span class="badge badge-pulse">{% trans "new" %}</span> {% endif %} </div> - + <div class="store-list-item-size"> {{ f.human_readable_size }} </div> @@ -122,12 +122,12 @@ </dl> </div> <div class="col-sm-2" style="text-align: right;"> - <a href="{% url "dashboard.views.store-download" %}?path={{ f.path }}" + <a href="{% url "dashboard.views.store-download" %}?path={{ f.path|urlencode }}" class="btn btn-primary btn-sm store-download-button"> <i class="fa fa-download"></i> {% trans "Download" %} </a> - <a href="{% url "dashboard.views.store-remove" %}?path={{ f.path }}" + <a href="{% url "dashboard.views.store-remove" %}?path={{ f.path|urlencode }}" class="btn btn-danger btn-xs store-remove-button"> <i class="fa fa-times"></i> {% trans "Remove" %} diff --git a/circle/dashboard/templates/dashboard/template-edit.html b/circle/dashboard/templates/dashboard/template-edit.html index d6bc6b5..64963e7 100644 --- a/circle/dashboard/templates/dashboard/template-edit.html +++ b/circle/dashboard/templates/dashboard/template-edit.html @@ -11,7 +11,9 @@ <div class="col-md-7"> <div class="panel panel-default"> <div class="panel-heading"> - <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a> + <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}"> + {% trans "Back" %} + </a> <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Edit template" %}</h3> </div> <div class="panel-body"> @@ -65,6 +67,38 @@ </div> <div class="col-md-5"> + {% if is_owner %} + <div class="panel panel-default"> + <div class="panel-heading"> + <a href="{% url "dashboard.views.template-delete" pk=object.pk %}" + class="btn btn-xs btn-danger pull-right"> + {% trans "Delete" %} + </a> + <h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete template" %}</h4> + </div> + </div> + {% endif %} + + <div class="panel panel-default"> + <div class="panel-heading"> + <h4 class="no-margin"><i class="fa fa-user"></i> {% trans "Owner" %}</h4> + </div> + <div class="panel-body"> + {% if user == object.owner %} + {% blocktrans %}You are the current owner of this template.{% endblocktrans %} + {% else %} + {% url "dashboard.views.profile" username=object.owner.username as url %} + {% blocktrans with owner=object.owner name=object.owner.get_full_name%} + The current owner of this template is <a href="{{url}}">{{name}} ({{owner}})</a>. + {% endblocktrans %} + {% endif %} + {% if user == object.owner or user.is_superuser %} + <a href="{% url "dashboard.views.template-transfer-ownership" object.pk %}" + class="btn btn-link tx-tpl-ownership">{% trans "Transfer ownership..." %}</a> + {% endif %} + </div> + </div> + <div class="panel panel-default"> <div class="panel-heading"> <h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4> diff --git a/circle/dashboard/templates/dashboard/template-tx-owner.html b/circle/dashboard/templates/dashboard/template-tx-owner.html new file mode 100644 index 0000000..91e82af --- /dev/null +++ b/circle/dashboard/templates/dashboard/template-tx-owner.html @@ -0,0 +1,16 @@ +{% load i18n %} + +<div class="pull-right"> + <form action="{% url "dashboard.views.template-transfer-ownership" pk=object.pk %}" method="POST" style="max-width: 400px;"> + {% csrf_token %} + <label> + {{ form.name.label }} + </label> + <div class="input-group"> + {{form.name}} + <div class="input-group-btn"> + <input type="submit" value="{% trans "Save" %}" class="btn btn-primary"> + </div> + </div> + </form> +</div> diff --git a/circle/dashboard/templates/dashboard/vm-detail.html b/circle/dashboard/templates/dashboard/vm-detail.html index 8d9b43f..ef7752a 100644 --- a/circle/dashboard/templates/dashboard/vm-detail.html +++ b/circle/dashboard/templates/dashboard/vm-detail.html @@ -6,14 +6,25 @@ {% block content %} {% if instance.is_base %} -<div class="alert alert-info alert-new-template"> - <strong>{% trans "This is the master vm of your new template" %}</strong> - <div id="vm-details-template-tour-button" class="pull-right"> - <a href="#" class="btn btn-default btn-lg pull-right vm-details-start-template-tour"> +<div class="alert alert-info alert-new-template" id="alert-new-template" style="position: relative;"> + <form action="{% url "dashboard.views.vm-toggle-tutorial" pk=instance.pk %}" + method="POST"> + {% csrf_token %} + <input name="hidden" type="hidden" + value="{{ hide_tutorial|yesno:"false,true" }}"/> + <button type="submit" + id="dashboard-tutorial-toggle" class="btn btn-sm pull-right btn-success"> + <i class="fa fa-caret-{% if hide_tutorial %}down{% else %}up{% endif %}"></i> + {% trans "Toggle tutorial panel" %} + </button> + + <a href="#" class="btn btn-default btn-sm pull-right" + id="vm-details-start-template-tour"> <i class="fa fa-play"></i> {% trans "Start template tutorial" %} </a> - </div> - <ol> + </form> + <strong>{% trans "This is the master vm of your new template" %}</strong> + <ol {% if hide_tutorial %}style="display: none;"{% endif %}> <li>{% trans "Modify the virtual machine to suit your needs <strong>(optional)</strong>" %} <ul> <li>{% trans "Change the description" %}</li> @@ -59,13 +70,20 @@ {{ instance.name }} </div> <small>{{ instance.primary_host.get_fqdn }}</small> + <small class="dashboard-vm-favourite" style="line-height: 39.6px;" data-vm="{{ instance.pk }}"> + {% if fav %} + <i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i> + {% else %} + <i class="fa fa-star-o text-primary title-favourite" title="{% trans "Mark as favorite" %}"></i> + {% endif %} + </small> </h1> <div style="clear: both;"></div> </div> <div class="row"> <div class="col-md-4" id="vm-info-pane"> <div class="big"> - <span id="vm-details-state" class="label label-success"> + <span id="vm-details-state" class="label label-success" data-status="{{ instance.status }}"> <i class="fa {% if is_new_state %} fa-spinner fa-spin @@ -110,7 +128,14 @@ <dd style="font-size: 10px; text-align: right; padding-top: 8px;"> <div id="vm-details-pw-reset"> {% with op=op.password_reset %}{% if op %} - <a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" class="operation operation-{{op.op}}" data-disabled="{% if op.disabled %}true" title="{% trans "Start the VM to change the password." %}"{% else %}false" {% endif %}>{% trans "Generate new password!" %}</a> + <a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" + class="operation operation-{{op.op}}" + {% if op.disabled %} + data-disabled="true" + title="{% if instance.has_agent %}{% trans "Start the VM to change the password." %}{% else %}{% trans "This machine has no agent installed." %}{% endif %}" + {% endif %}> + {% trans "Generate new password!" %} + </a> {% endif %}{% endwith %} </div> </dd> @@ -138,7 +163,7 @@ </div> {% endfor %} {% if instance.get_connect_uri %} - <div id="dashboard-vm-details-connect" class="operation-wrapper"> + <div id="dashboard-vm-details-connect" class="operation-wrapper"> {% if client_download %} <a id="dashboard-vm-details-connect-button" class="btn btn-xs btn-default operation " href="{{ instance.get_connect_uri}}" title="{% trans "Connect via the CIRCLE Client" %}"> <i class="fa fa-external-link"></i> {% trans "Connect" %} @@ -202,7 +227,7 @@ {% endblock %} {% block extra_js %} - <script src="{{ STATIC_URL }}dashboard/bootstrap-tour.min.js"></script> + <script src="{{ STATIC_URL }}dashboard/introjs/intro.min.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-details.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-common.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-console.js"></script> diff --git a/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html b/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html index 84a3a24..7f80150 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html +++ b/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html @@ -3,7 +3,8 @@ <div id="activity-timeline" class="timeline"> {% for a in activities %} -<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}"> +<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" + data-activity-id="{{ a.pk }}" data-activity-code="{{ a.activity_code }}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i> </span> @@ -33,7 +34,8 @@ {% if a.children.count > 0 %} <div class="sub-timeline"> {% for s in a.children.all %} - <div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}"> + <div data-activity-id="{{ s.pk }}" data-activity-code="{{ s.activity_code }}" + class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}"> <span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}> <a href="{{ s.get_absolute_url }}"> {{ s.readable_name|get_text:user|capfirst }}</a></span> – diff --git a/circle/dashboard/templates/dashboard/vm-detail/_network-port-add.html b/circle/dashboard/templates/dashboard/vm-detail/_network-port-add.html index 7601a01..8cae3ae 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/_network-port-add.html +++ b/circle/dashboard/templates/dashboard/vm-detail/_network-port-add.html @@ -1,13 +1,14 @@ {% load i18n %} <div class="vm-details-network-port-add pull-right"> - <form action="" method="POST"> + <form action="{{ op.add_port.get_url }}" method="POST"> {% csrf_token %} - <input type="hidden" name="host_pk" value="{{ i.host.pk }}"/> + <input type="hidden" name="host" value="{{ i.host.pk }}"/> <div class="input-group input-group-sm"> <span class="input-group-addon"> <i class="fa fa-plus"></i> <i class="fa fa-long-arrow-right"></i> </span> - <input type="text" class="form-control" size="5" style="width: 80px;" name="port"/> + <input type="number" class="form-control" size="5" min="1" max="65535" + style="width: 80px;" name="port" required/> <span class="input-group-addon">/</span> <select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select> <div class="input-group-btn"> diff --git a/circle/dashboard/templates/dashboard/vm-detail/access.html b/circle/dashboard/templates/dashboard/vm-detail/access.html index b716552..065770d 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/access.html +++ b/circle/dashboard/templates/dashboard/vm-detail/access.html @@ -4,8 +4,9 @@ {% if user == instance.owner %} {% blocktrans %}You are the current owner of this instance.{% endblocktrans %} {% else %} - {% blocktrans with owner=instance.owner %} - The current owner of this instance is {{owner}}. + {% url "dashboard.views.profile" username=instance.owner.username as url %} + {% blocktrans with owner=instance.owner name=instance.owner.get_full_name%} + The current owner of this instance is <a href="{{url}}">{{name}} ({{owner}})</a>. {% endblocktrans %} {% endif %} {% if user == instance.owner or user.is_superuser %} 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 %} <div class="row"> <div class="col-md-4"> - <dl> + <dl id="home_name_and_description"> <dt>{% trans "System" %}:</dt> <dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd> <dt style="margin-top: 5px;"> @@ -52,30 +52,34 @@ </dd> </dl> - <h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} - <span id="vm-details-renew-op"> - {% with op=op.renew %}{% if op %} - <a href="{{op.get_url}}" class="btn btn-success btn-xs - operation operation-{{op.op}}"> - <i class="fa fa-{{op.icon}}"></i> - {{op.name}} </a> - {% endif %}{% endwith %} - </span> - </h4> - <dl> - <dt>{% trans "Suspended at:" %}</dt> - <dd> - <span title="{{ instance.time_of_suspend }}"> - <i class="fa fa-moon-o"></i> {{ instance.time_of_suspend|timeuntil }} + <div id="home_expiration_and_lease"> + <h4> + {% trans "Expiration" %} + {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} + <span id="vm-details-renew-op"> + {% with op=op.renew %}{% if op %} + <a href="{{op.get_url}}" class="btn btn-success btn-xs + operation operation-{{op.op}}"> + <i class="fa fa-{{op.icon}}"></i> + {{op.name}} </a> + {% endif %}{% endwith %} </span> - </dd> - <dt>{% trans "Destroyed at:" %}</dt> - <dd> - <span title="{{ instance.time_of_delete }}"> - <i class="fa fa-times"></i> {{ instance.time_of_delete|timeuntil }} - </span> - </dd> - </dl> + </h4> + <dl> + <dt>{% trans "Suspended at:" %}</dt> + <dd> + <span title="{{ instance.time_of_suspend }}"> + <i class="fa fa-moon-o"></i> {{ instance.time_of_suspend|timeuntil }} + </span> + </dd> + <dt>{% trans "Destroyed at:" %}</dt> + <dd> + <span title="{{ instance.time_of_delete }}"> + <i class="fa fa-times"></i> {{ instance.time_of_delete|timeuntil }} + </span> + </dd> + </dl> + </div> <div style="font-weight: bold;">{% trans "Tags" %}</div> <div id="vm-details-tags" style="margin-bottom: 20px;"> diff --git a/circle/dashboard/templates/dashboard/vm-detail/network.html b/circle/dashboard/templates/dashboard/vm-detail/network.html index 9e49384..d4d63b8 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/network.html +++ b/circle/dashboard/templates/dashboard/vm-detail/network.html @@ -78,7 +78,7 @@ {{ l.private }}/{{ l.proto }} </td> <td> - <a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> + <a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> </td> </tr> {% endif %} @@ -110,7 +110,7 @@ {{ l.private }}/{{ l.proto }} </td> <td> - <a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> + <a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> </td> </tr> {% endif %} 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 %} </form> - <hr /> - -<div class="row" id="vm-details-resources-disk"> - <div class="col-sm-11"> - <h3> - {% trans "Disks" %} - <div class="pull-right"> - <div id="disk-ops"> - {% include "dashboard/vm-detail/_disk-operations.html" %} - </div> +<div id="vm-details-resources-disk"> + <h3> + {% trans "Disks" %} + <div class="pull-right"> + <div id="disk-ops"> + {% include "dashboard/vm-detail/_disk-operations.html" %} </div> - </h3> - - <div class="row" id="vm-details-disk-add-for-form"></div> - - {% if not instance.disks.all %} - {% trans "No disks are added!" %} - {% endif %} - {% for d in instance.disks.all %} - <h4 class="list-group-item-heading dashboard-vm-details-network-h3"> - {% with long_remove=True %} - {% include "dashboard/_disk-list-element.html" %} - {% endwith %} - </h4> - {% endfor %} - </div> + </div> + </h3> + + {% if not instance.disks.all %} + {% trans "No disks are added." %} + {% endif %} + {% for d in instance.disks.all %} + <h4 class="list-group-item-heading dashboard-vm-details-network-h3"> + {% with long_remove=True %} + {% include "dashboard/_disk-list-element.html" %} + {% endwith %} + </h4> + {% endfor %} </div> {% if user.is_superuser %} diff --git a/circle/dashboard/tests/test_templates.py b/circle/dashboard/tests/test_templates.py new file mode 100644 index 0000000..2592bc4 --- /dev/null +++ b/circle/dashboard/tests/test_templates.py @@ -0,0 +1,52 @@ +# Copyright 2014 Budapest University of Technology and Economics (BME IK) +# +# This file is part of CIRCLE Cloud. +# +# CIRCLE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along +# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. + +from os import listdir +from os.path import isfile, isdir, join +import unittest + +from django.conf import settings +from django.template import Template, Context, VariableDoesNotExist +from django.template.loader import find_template_loader +from django.core.urlresolvers import NoReverseMatch + + +class TemplateSyntaxTestCase(unittest.TestCase): + + def test_templates(self): + """Test all templates for syntax errors.""" + for loader_name in settings.TEMPLATE_LOADERS: + print loader_name + loader = find_template_loader(loader_name) + self._test_dir(loader.get_template_sources('')) + + def _test_dir(self, dir, path="/"): + for i in dir: + i = join(path, i) + if isfile(i): + self._test_template(join(path, i)) + elif isdir(i): + print "%s:" % i + self._test_dir(listdir(i), i) + + def _test_template(self, path): + print path + try: + Template(open(path).read()).render(Context({})) + except (NoReverseMatch, VariableDoesNotExist, KeyError, AttributeError, + ValueError, ) as e: + print e diff --git a/circle/dashboard/tests/test_views.py b/circle/dashboard/tests/test_views.py index 8268da3..7aca07e 100644 --- a/circle/dashboard/tests/test_views.py +++ b/circle/dashboard/tests/test_views.py @@ -26,7 +26,8 @@ from django.contrib.auth import authenticate from dashboard.views import VmAddInterfaceView from vm.models import Instance, InstanceTemplate, Lease, Node, Trait -from vm.operations import WakeUpOperation, AddInterfaceOperation +from vm.operations import (WakeUpOperation, AddInterfaceOperation, + AddPortOperation) from ..models import Profile from firewall.models import Vlan, Host, VlanGroup from mock import Mock, patch @@ -299,7 +300,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): @@ -335,51 +336,48 @@ class VmDetailTest(LoginMixin, TestCase): self.login(c, "user2") inst = Instance.objects.get(pk=1) inst.set_level(self.u2, 'owner') - response = c.post("/dashboard/vm/1/", {'port': True, - 'proto': 'tcp', - 'port': '1337'}) + vlan = Vlan.objects.get(id=1) + vlan.set_level(self.u2, 'user') + inst.add_interface(user=self.u2, vlan=vlan) + host = Host.objects.get( + interface__in=inst.interface_set.all()) + with patch.object(AddPortOperation, 'async') as mock_method: + mock_method.side_effect = inst.add_port + response = c.post("/dashboard/vm/1/op/add_port/", { + 'proto': 'tcp', 'host': host.pk, 'port': '1337'}) self.assertEqual(response.status_code, 403) def test_unpermitted_add_port_wo_obj_levels(self): c = Client() self.login(c, "user2") - self.u2.user_permissions.add(Permission.objects.get( - name='Can configure port forwards.')) - response = c.post("/dashboard/vm/1/", {'port': True, - 'proto': 'tcp', - 'port': '1337'}) - self.assertEqual(response.status_code, 403) - - def test_unpermitted_add_port_w_bad_host(self): - c = Client() - self.login(c, "user2") inst = Instance.objects.get(pk=1) - inst.set_level(self.u2, 'owner') + vlan = Vlan.objects.get(id=1) + vlan.set_level(self.u2, 'user') + inst.add_interface(user=self.u2, vlan=vlan, system=True) + host = Host.objects.get( + interface__in=inst.interface_set.all()) self.u2.user_permissions.add(Permission.objects.get( name='Can configure port forwards.')) - response = c.post("/dashboard/vm/1/", {'proto': 'tcp', - 'host_pk': '9999', - 'port': '1337'}) + with patch.object(AddPortOperation, 'async') as mock_method: + mock_method.side_effect = inst.add_port + response = c.post("/dashboard/vm/1/op/add_port/", { + 'proto': 'tcp', 'host': host.pk, 'port': '1337'}) + assert not mock_method.called self.assertEqual(response.status_code, 403) - def test_permitted_add_port_w_unhandled_exception(self): + def test_unpermitted_add_port_w_bad_host(self): c = Client() self.login(c, "user2") inst = Instance.objects.get(pk=1) inst.set_level(self.u2, 'owner') - vlan = Vlan.objects.get(id=1) - vlan.set_level(self.u2, 'user') - inst.add_interface(user=self.u2, vlan=vlan) - host = Host.objects.get( - interface__in=inst.interface_set.all()) self.u2.user_permissions.add(Permission.objects.get( name='Can configure port forwards.')) - port_count = len(host.list_ports()) - response = c.post("/dashboard/vm/1/", {'proto': 'tcp', - 'host_pk': host.pk, - 'port': 'invalid_port'}) - self.assertEqual(response.status_code, 302) - self.assertEqual(len(host.list_ports()), port_count) + with patch.object(AddPortOperation, 'async') as mock_method: + mock_method.side_effect = inst.add_port + response = c.post("/dashboard/vm/1/op/add_port/", { + 'proto': 'tcp', 'host': '9999', 'port': '1337'}) + assert not mock_method.called + self.assertEqual(response.status_code, 200) def test_permitted_add_port(self): c = Client() @@ -394,9 +392,11 @@ class VmDetailTest(LoginMixin, TestCase): self.u2.user_permissions.add(Permission.objects.get( name='Can configure port forwards.')) port_count = len(host.list_ports()) - response = c.post("/dashboard/vm/1/", {'proto': 'tcp', - 'host_pk': host.pk, - 'port': '1337'}) + with patch.object(AddPortOperation, 'async') as mock_method: + mock_method.side_effect = inst.add_port + response = c.post("/dashboard/vm/1/op/add_port/", { + 'proto': 'tcp', 'host': host.pk, 'port': '1337'}) + assert mock_method.called self.assertEqual(response.status_code, 302) self.assertEqual(len(host.list_ports()), port_count + 1) @@ -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..9bbc5b2 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -26,9 +26,9 @@ from .views import ( InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, NodeDetailView, NodeList, NodeStatus, - NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, - TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, - TransferOwnershipView, vm_activity, VmCreate, VmDetailView, + NotificationView, TemplateAclUpdateView, TemplateCreate, + TemplateDelete, TemplateDetail, TemplateList, + vm_activity, VmCreate, VmDetailView, VmDetailVncTokenView, VmList, DiskRemoveView, get_disk_download_status, InterfaceDeleteView, GroupRemoveUserView, @@ -45,8 +45,11 @@ from .views import ( VmTraitsUpdate, VmRawDataUpdate, GroupPermissionsView, LeaseAclUpdateView, + toggle_template_tutorial, ClientCheck, TokenLogin, VmGraphView, NodeGraphView, NodeListGraphView, + TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView, + TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView, ) from .views.vm import vm_ops, vm_mass_ops from .views.node import node_ops @@ -77,15 +80,15 @@ urlpatterns = patterns( name="dashboard.views.template-list"), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), name="dashboard.views.template-delete"), - url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), - name='dashboard.views.remove-port'), + url(r'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(), + name='dashboard.views.template-transfer-ownership'), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), name='dashboard.views.detail'), url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(), name='dashboard.views.detail-vnc'), url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance), name='dashboard.views.vm-acl'), - url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(), + url(r'^vm/(?P<pk>\d+)/tx/$', TransferInstanceOwnershipView.as_view(), name='dashboard.views.vm-transfer-ownership'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/create/$', VmCreate.as_view(), @@ -99,14 +102,20 @@ urlpatterns = patterns( name='dashboard.views.vm-traits'), url(r'^vm/(?P<pk>\d+)/raw_data/$', VmRawDataUpdate.as_view(), name='dashboard.views.vm-raw-data'), + url(r'^vm/(?P<pk>\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<pk>\d+)/$', NodeDetailView.as_view(), name='dashboard.views.node-detail'), url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(), name='dashboard.views.node-addtrait'), - url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(), + url(r'^vm/tx/(?P<key>.*)/?$', + TransferInstanceOwnershipConfirmView.as_view(), name='dashboard.views.vm-transfer-ownership-confirm'), + url(r'^template/tx/(?P<key>.*)/?$', + TransferTemplateOwnershipConfirmView.as_view(), + name='dashboard.views.template-transfer-ownership-confirm'), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), name="dashboard.views.delete-node"), url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.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/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 d5fd13b..568d903 100644 --- a/circle/dashboard/views/template.py +++ b/circle/dashboard/views/template.py @@ -26,13 +26,13 @@ from django.core.urlresolvers import reverse, reverse_lazy from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, get_object_or_404 -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ugettext_noop from django.views.generic import ( TemplateView, CreateView, DeleteView, UpdateView, ) from braces.views import ( - LoginRequiredMixin, PermissionRequiredMixin, SuperuserRequiredMixin, + LoginRequiredMixin, PermissionRequiredMixin, ) from django_tables2 import SingleTableView @@ -44,7 +44,10 @@ from ..forms import ( ) from ..tables import TemplateListTable, LeaseListTable -from .util import AclUpdateView, FilterMixin +from .util import ( + AclUpdateView, FilterMixin, + TransferOwnershipConfirmView, TransferOwnershipView, +) logger = logging.getLogger(__name__) @@ -240,6 +243,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'): @@ -382,13 +395,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" @@ -404,8 +421,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): @@ -431,10 +461,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() @@ -449,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): else: messages.success(request, success_message) return HttpResponseRedirect(success_url) + + +class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView): + template = "dashboard/confirm/transfer-template-ownership.html" + model = InstanceTemplate + + +class TransferTemplateOwnershipView(TransferOwnershipView): + confirm_view = TransferTemplateOwnershipConfirmView + model = InstanceTemplate + notification_msg = ugettext_noop( + '%(user)s offered you to take the ownership of ' + 'his/her template called %(instance)s. ' + '<a href="%(token)s" ' + 'class="btn btn-success btn-small">Accept</a>') + token_url = 'dashboard.views.template-transfer-ownership-confirm' + template = "dashboard/template-tx-owner.html" diff --git a/circle/dashboard/views/util.py b/circle/dashboard/views/util.py index c870c4a..73e74d7 100644 --- a/circle/dashboard/views/util.py +++ b/circle/dashboard/views/util.py @@ -24,14 +24,15 @@ from urlparse import urljoin from django.conf import settings from django.contrib.auth.models import User, Group -from django.core.exceptions import PermissionDenied +from django.core import signing +from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.urlresolvers import reverse from django.contrib import messages from django.contrib.auth.views import redirect_to_login from django.db.models import Q -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import redirect -from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponse, Http404, HttpResponseRedirect +from django.shortcuts import redirect, render +from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.views.generic import DetailView, View from django.views.generic.detail import SingleObjectMixin @@ -40,7 +41,8 @@ from braces.views._access import AccessMixin from celery.exceptions import TimeoutError from common.models import HumanReadableException, HumanReadableObject -from ..models import GroupProfile +from ..models import GroupProfile, Profile +from ..forms import TransferOwnershipForm logger = logging.getLogger(__name__) saml_available = hasattr(settings, "SAML_CONFIG") @@ -563,3 +565,132 @@ class GraphMixin(object): def absolute_url(url): return urljoin(settings.DJANGO_URL, url) + + +class TransferOwnershipView(CheckedDetailView, DetailView): + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/_modal.html'] + else: + return ['dashboard/nojs-wrapper.html'] + + def get_context_data(self, *args, **kwargs): + context = super(TransferOwnershipView, self).get_context_data( + *args, **kwargs) + context['form'] = TransferOwnershipForm() + context.update({ + 'box_title': _("Transfer ownership"), + 'ajax_title': True, + 'template': self.template, + }) + return context + + def post(self, request, *args, **kwargs): + form = TransferOwnershipForm(request.POST) + if not form.is_valid(): + return self.get(request) + try: + new_owner = search_user(request.POST['name']) + except User.DoesNotExist: + messages.error(request, _('Can not find specified user.')) + return self.get(request, *args, **kwargs) + except KeyError: + raise SuspiciousOperation() + + obj = self.get_object() + if not (obj.owner == request.user or + request.user.is_superuser): + raise PermissionDenied() + + token = signing.dumps( + (obj.pk, new_owner.pk), + salt=self.confirm_view.get_salt()) + token_path = reverse(self.token_url, args=[token]) + try: + new_owner.profile.notify( + ugettext_noop('Ownership offer'), + self.notification_msg, + {'instance': obj, 'token': token_path}) + except Profile.DoesNotExist: + messages.error(request, _('Can not notify selected user.')) + else: + messages.success(request, + _('User %s is notified about the offer.') % ( + unicode(new_owner), )) + + return redirect(obj.get_absolute_url()) + + +class TransferOwnershipConfirmView(LoginRequiredMixin, View): + """User can accept an ownership offer.""" + + max_age = 3 * 24 * 3600 + success_message = _("Ownership successfully transferred to you.") + + @classmethod + def get_salt(cls): + return unicode(cls) + unicode(cls.model) + + def get(self, request, key, *args, **kwargs): + """Confirm ownership transfer based on token. + """ + logger.debug('Confirm dialog for token %s.', key) + try: + instance, new_owner = self.get_instance(key, request.user) + except PermissionDenied: + messages.error(request, _('This token is for an other user.')) + raise + except SuspiciousOperation: + messages.error(request, _('This token is invalid or has expired.')) + raise PermissionDenied() + return render(request, self.template, + dictionary={'instance': instance, 'key': key}) + + def change_owner(self, instance, new_owner): + instance.owner = new_owner + instance.clean() + instance.save() + + def post(self, request, key, *args, **kwargs): + """Really transfer ownership based on token. + """ + instance, owner = self.get_instance(key, request.user) + + old = instance.owner + self.change_owner(instance, request.user) + messages.success(request, self.success_message) + logger.info('Ownership of %s transferred from %s to %s.', + unicode(instance), unicode(old), unicode(request.user)) + if old.profile: + old.profile.notify( + ugettext_noop('Ownership accepted'), + ugettext_noop('Your ownership offer of %(instance)s has been ' + 'accepted by %(user)s.'), + {'instance': instance}) + return redirect(instance.get_absolute_url()) + + def get_instance(self, key, user): + """Get object based on signed token. + """ + try: + instance, new_owner = ( + signing.loads(key, max_age=self.max_age, + salt=self.get_salt())) + except (signing.BadSignature, ValueError, TypeError) as e: + logger.error('Tried invalid token. Token: %s, user: %s. %s', + key, unicode(user), unicode(e)) + raise SuspiciousOperation() + + try: + instance = self.model.objects.get(id=instance) + except self.model.DoesNotExist as e: + logger.error('Tried token to nonexistent instance %d. ' + 'Token: %s, user: %s. %s', + instance, key, unicode(user), unicode(e)) + raise Http404() + + if new_owner != user.pk: + logger.error('%s (%d) tried the token for %s. Token: %s.', + unicode(user), user.pk, new_owner, key) + raise PermissionDenied() + return (instance, new_owner) diff --git a/circle/dashboard/views/vm.py b/circle/dashboard/views/vm.py index 38f3dfe..b02153f 100644 --- a/circle/dashboard/views/vm.py +++ b/circle/dashboard/views/vm.py @@ -24,11 +24,12 @@ 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 from django.http import HttpResponse, Http404, HttpResponseRedirect -from django.shortcuts import redirect, get_object_or_404, render +from django.shortcuts import redirect, get_object_or_404 from django.template import RequestContext from django.template.loader import render_to_string from django.utils.translation import ( @@ -36,7 +37,7 @@ from django.utils.translation import ( ) from django.views.decorators.http import require_GET from django.views.generic import ( - UpdateView, ListView, TemplateView, DeleteView, DetailView, View, + UpdateView, ListView, TemplateView, DeleteView ) from braces.views import SuperuserRequiredMixin, LoginRequiredMixin @@ -53,16 +54,18 @@ from vm.models import ( ) from .util import ( CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView, - FormOperationMixin, FilterMixin, search_user, GraphMixin, + FormOperationMixin, FilterMixin, GraphMixin, + TransferOwnershipConfirmView, TransferOwnershipView, ) from ..forms import ( AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm, VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, - TransferOwnershipForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, - VmMigrateForm, + VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, + VmMigrateForm, VmDeployForm, + VmPortRemoveForm, VmPortAddForm, ) -from ..models import Favourite, Profile +from ..models import Favourite logger = logging.getLogger(__name__) @@ -102,13 +105,17 @@ 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, + 'fav': instance.favourite_set.filter(user=user).exists(), }) # activity data @@ -170,7 +177,6 @@ class VmDetailView(GraphMixin, CheckedDetailView): 'new_description': self.__set_description, 'new_tag': self.__add_tag, 'to_remove': self.__remove_tag, - 'port': self.__add_port, 'abort_operation': self.__abort_operation, } for k, v in options.iteritems(): @@ -266,40 +272,6 @@ class VmDetailView(GraphMixin, CheckedDetailView): return redirect(reverse_lazy("dashboard.views.detail", kwargs={'pk': self.object.pk})) - def __add_port(self, request): - object = self.get_object() - if not (object.has_level(request.user, "operator") and - request.user.has_perm('vm.config_ports')): - raise PermissionDenied() - - port = request.POST.get("port") - proto = request.POST.get("proto") - - try: - error = None - interfaces = object.interface_set.all() - host = Host.objects.get(pk=request.POST.get("host_pk"), - interface__in=interfaces) - host.add_port(proto, private=port) - except Host.DoesNotExist: - logger.error('Tried to add port to nonexistent host %d. User: %s. ' - 'Instance: %s', request.POST.get("host_pk"), - unicode(request.user), object) - raise PermissionDenied() - except ValueError: - error = _("There is a problem with your input.") - except Exception as e: - error = _("Unknown error.") - logger.error(e) - - if request.is_ajax(): - pass - else: - if error: - messages.error(request, error) - return redirect(reverse_lazy("dashboard.views.detail", - kwargs={'pk': self.get_object().pk})) - def __abort_operation(self, request): self.object = self.get_object() @@ -446,6 +418,62 @@ class VmMigrateView(FormOperationMixin, VmOperationView): return val +class VmPortRemoveView(FormOperationMixin, VmOperationView): + + template_name = 'dashboard/_vm-remove-port.html' + op = 'remove_port' + show_in_toolbar = False + with_reload = True + wait_for_result = 0.5 + icon = 'times' + effect = "danger" + form_class = VmPortRemoveForm + + def get_form_kwargs(self): + instance = self.get_op().instance + choices = Rule.portforwards().filter( + host__interface__instance=instance) + rule_pk = self.request.GET.get('rule') + if rule_pk: + try: + default = choices.get(pk=rule_pk) + except (ValueError, Rule.DoesNotExist): + raise Http404() + else: + default = None + + val = super(VmPortRemoveView, self).get_form_kwargs() + val.update({'choices': choices, 'default': default}) + return val + + +class VmPortAddView(FormOperationMixin, VmOperationView): + + op = 'add_port' + show_in_toolbar = False + with_reload = True + wait_for_result = 0.5 + icon = 'plus' + effect = "success" + form_class = VmPortAddForm + + def get_form_kwargs(self): + instance = self.get_op().instance + choices = Host.objects.filter(interface__instance=instance) + host_pk = self.request.GET.get('host') + if host_pk: + try: + default = choices.get(pk=host_pk) + except (ValueError, Host.DoesNotExist): + raise Http404() + else: + default = None + + val = super(VmPortAddView, self).get_form_kwargs() + val.update({'choices': choices, 'default': default}) + return val + + class VmSaveView(FormOperationMixin, VmOperationView): op = 'save_as_template' @@ -457,6 +485,10 @@ class VmSaveView(FormOperationMixin, VmOperationView): 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 @@ -605,7 +637,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 @@ -628,9 +659,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( @@ -645,7 +690,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), @@ -662,6 +707,8 @@ vm_ops = OrderedDict([ op='remove_disk', form_class=VmDiskRemoveForm, icon='times', effect="danger")), ('add_interface', VmAddInterfaceView), + ('remove_port', VmPortRemoveView), + ('add_port', VmPortAddView), ('renew', VmRenewView), ('resources_change', VmResourcesChangeView), ('password_reset', VmOperationView.factory( @@ -1145,52 +1192,6 @@ def get_disk_download_status(request, pk): ) -class PortDelete(LoginRequiredMixin, DeleteView): - model = Rule - pk_url_kwarg = 'rule' - - 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(PortDelete, self).get_context_data(**kwargs) - rule = kwargs.get('object') - instance = rule.host.interface_set.get().instance - context['title'] = _("Port delete confirmation") - context['text'] = _("Are you sure you want to close %(port)d/" - "%(proto)s on %(vm)s?" % {'port': rule.dport, - 'proto': rule.proto, - 'vm': instance}) - return context - - def delete(self, request, *args, **kwargs): - rule = Rule.objects.get(pk=kwargs.get("rule")) - instance = rule.host.interface_set.get().instance - if not instance.has_level(request.user, 'owner'): - raise PermissionDenied() - - super(PortDelete, self).delete(request, *args, **kwargs) - - success_url = self.get_success_url() - success_message = _("Port 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#network" % success_url) - - def get_success_url(self): - return reverse_lazy('dashboard.views.detail', - kwargs={'pk': self.kwargs.get("pk")}) - - class ClientCheck(LoginRequiredMixin, TemplateView): def get_template_names(self): @@ -1285,136 +1286,36 @@ class FavouriteView(TemplateView): return HttpResponse("Added.") -class TransferOwnershipView(CheckedDetailView, DetailView): +class TransferInstanceOwnershipConfirmView(TransferOwnershipConfirmView): + template = "dashboard/confirm/transfer-instance-ownership.html" model = Instance - def get_template_names(self): - if self.request.is_ajax(): - return ['dashboard/_modal.html'] - else: - return ['dashboard/nojs-wrapper.html'] - - def get_context_data(self, *args, **kwargs): - context = super(TransferOwnershipView, self).get_context_data( - *args, **kwargs) - context['form'] = TransferOwnershipForm() - context.update({ - 'box_title': _("Transfer ownership"), - 'ajax_title': True, - 'template': "dashboard/vm-detail/tx-owner.html", - }) - return context - - def post(self, request, *args, **kwargs): - form = TransferOwnershipForm(request.POST) - if not form.is_valid(): - return self.get(request) - try: - new_owner = search_user(request.POST['name']) - except User.DoesNotExist: - messages.error(request, _('Can not find specified user.')) - return self.get(request, *args, **kwargs) - except KeyError: - raise SuspiciousOperation() - - obj = self.get_object() - if not (obj.owner == request.user or - request.user.is_superuser): - raise PermissionDenied() - - token = signing.dumps((obj.pk, new_owner.pk), - salt=TransferOwnershipConfirmView.get_salt()) - token_path = reverse( - 'dashboard.views.vm-transfer-ownership-confirm', args=[token]) - try: - new_owner.profile.notify( - ugettext_noop('Ownership offer'), - ugettext_noop('%(user)s offered you to take the ownership of ' - 'his/her virtual machine called %(instance)s. ' - '<a href="%(token)s" ' - 'class="btn btn-success btn-small">Accept</a>'), - {'instance': obj, 'token': token_path}) - except Profile.DoesNotExist: - messages.error(request, _('Can not notify selected user.')) - else: - messages.success(request, - _('User %s is notified about the offer.') % ( - unicode(new_owner), )) - - return redirect(reverse_lazy("dashboard.views.detail", - kwargs={'pk': obj.pk})) - - -class TransferOwnershipConfirmView(LoginRequiredMixin, View): - """User can accept an ownership offer.""" + def change_owner(self, instance, new_owner): + with instance.activity( + code_suffix='ownership-transferred', + readable_name=ugettext_noop("transfer ownership"), + concurrency_check=False, user=new_owner): + super(TransferInstanceOwnershipConfirmView, self).change_owner( + instance, new_owner) - max_age = 3 * 24 * 3600 - success_message = _("Ownership successfully transferred to you.") - - @classmethod - def get_salt(cls): - return unicode(cls) - def get(self, request, key, *args, **kwargs): - """Confirm ownership transfer based on token. - """ - logger.debug('Confirm dialog for token %s.', key) - try: - instance, new_owner = self.get_instance(key, request.user) - except PermissionDenied: - messages.error(request, _('This token is for an other user.')) - raise - except SuspiciousOperation: - messages.error(request, _('This token is invalid or has expired.')) - raise PermissionDenied() - return render(request, - "dashboard/confirm/base-transfer-ownership.html", - dictionary={'instance': instance, 'key': key}) - - def post(self, request, key, *args, **kwargs): - """Really transfer ownership based on token. - """ - instance, owner = self.get_instance(key, request.user) - - old = instance.owner - with instance.activity(code_suffix='ownership-transferred', - concurrency_check=False, user=request.user): - instance.owner = request.user - instance.clean() - instance.save() - messages.success(request, self.success_message) - logger.info('Ownership of %s transferred from %s to %s.', - unicode(instance), unicode(old), unicode(request.user)) - if old.profile: - old.profile.notify( - ugettext_noop('Ownership accepted'), - ugettext_noop('Your ownership offer of %(instance)s has been ' - 'accepted by %(user)s.'), - {'instance': instance}) - return redirect(instance.get_absolute_url()) - - def get_instance(self, key, user): - """Get object based on signed token. - """ - try: - instance, new_owner = ( - signing.loads(key, max_age=self.max_age, - salt=self.get_salt())) - except (signing.BadSignature, ValueError, TypeError) as e: - logger.error('Tried invalid token. Token: %s, user: %s. %s', - key, unicode(user), unicode(e)) - raise SuspiciousOperation() - - try: - instance = Instance.objects.get(id=instance) - except Instance.DoesNotExist as e: - logger.error('Tried token to nonexistent instance %d. ' - 'Token: %s, user: %s. %s', - instance, key, unicode(user), unicode(e)) - raise Http404() - - if new_owner != user.pk: - logger.error('%s (%d) tried the token for %s. Token: %s.', - unicode(user), user.pk, new_owner, key) - raise PermissionDenied() - return (instance, new_owner) +class TransferInstanceOwnershipView(TransferOwnershipView): + confirm_view = TransferInstanceOwnershipConfirmView + model = Instance + notification_msg = ugettext_noop( + '%(user)s offered you to take the ownership of ' + 'his/her virtual machine called %(instance)s. ' + '<a href="%(token)s" ' + 'class="btn btn-success btn-small">Accept</a>') + token_url = 'dashboard.views.vm-transfer-ownership-confirm' + template = "dashboard/vm-detail/tx-owner.html" + + +@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 bc97eaf..4c595ff 100755 --- a/circle/fabfile.py +++ b/circle/fabfile.py @@ -84,6 +84,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) @@ -99,10 +103,12 @@ def pull(dir="~/circle/circle"): @roles('portal') -def update_portal(test=False): +def update_portal(test=False, git=True): "Update and restart portal+manager" with _stopped("portal", "manager"): - pull() + if git: + pull() + cleanup() pip("circle", "~/circle/requirements.txt") migrate() compile_things() @@ -111,6 +117,12 @@ def update_portal(test=False): @roles('portal') +def build_portal(): + "Update portal without pulling from git" + return update_portal(False, False) + + +@roles('portal') def stop_portal(test=False): "Stop portal and manager" _stop_services("portal", "manager") @@ -122,10 +134,15 @@ def update_node(): with _stopped("node", "agentdriver", "monitor-client"): pull("~/vmdriver") pip("vmdriver", "~/vmdriver/requirements/production.txt") + _cleanup("~/vmdriver") + pull("~/agentdriver") pip("agentdriver", "~/agentdriver/requirements.txt") + _cleanup("~/agentdriver") + pull("~/monitor-client") pip("monitor-client", "~/monitor-client/requirements.txt") + _cleanup("~/monitor-client") @parallel @@ -147,6 +164,18 @@ def checkout(vmdriver="master", agent="master"): run("git checkout %s" % agent) +@roles('portal') +def cleanup(): + "Clean pyc files of portal" + _cleanup() + + +def _cleanup(dir="~/circle/circle"): + "Clean pyc files" + with cd("~/circle/circle"): + run("find -name '*.py[co]' -exec rm -f {} +") + + def _stop_services(*services): "Stop given services (warn only if not running)" with settings(warn_only=True): @@ -175,3 +204,12 @@ def _stopped(*services): def _workon(name): return prefix("source ~/.virtualenvs/%s/bin/activate && " "source ~/.virtualenvs/%s/bin/postactivate" % (name, name)) + + +@roles('portal') +def install_bash_completion_script(): + sudo("wget https://raw.githubusercontent.com/marcelor/fabric-bash-" + "autocompletion/48baf5735bafbb2be5be8787d2c2c04a44b6cdb0/fab " + "-O /etc/bash_completion.d/fab") + print("To have bash completion instantly, run\n" + " source /etc/bash_completion.d/fab") diff --git a/circle/firewall/fields.py b/circle/firewall/fields.py index 47f5539..8da33a4 100644 --- a/circle/firewall/fields.py +++ b/circle/firewall/fields.py @@ -34,6 +34,10 @@ reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$') ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$') +class mac_custom(mac_unix): + word_fmt = '%.2X' + + class MACAddressFormField(forms.Field): default_error_messages = { 'invalid': _(u'Enter a valid MAC address. %s'), @@ -51,9 +55,6 @@ class MACAddressField(models.Field): description = _('MAC Address object') __metaclass__ = models.SubfieldBase - class mac_custom(mac_unix): - word_fmt = '%.2X' - def __init__(self, *args, **kwargs): kwargs['max_length'] = 17 super(MACAddressField, self).__init__(*args, **kwargs) @@ -65,7 +66,7 @@ class MACAddressField(models.Field): if isinstance(value, EUI): return value - return EUI(value, dialect=MACAddressField.mac_custom) + return EUI(value, dialect=mac_custom) def get_internal_type(self): return 'CharField' diff --git a/circle/firewall/models.py b/circle/firewall/models.py index cfa4a99..2a7e6ba 100644 --- a/circle/firewall/models.py +++ b/circle/firewall/models.py @@ -243,6 +243,13 @@ class Rule(models.Model): return retval + @classmethod + def portforwards(cls, host=None): + qs = cls.objects.filter(dport__isnull=False, direction='in') + if host is not None: + qs = qs.filter(host=host) + return qs + class Meta: verbose_name = _("rule") verbose_name_plural = _("rules") @@ -762,7 +769,7 @@ class Host(models.Model): Return a list of ports with forwarding rules set. """ retval = [] - for rule in self.rules.filter(dport__isnull=False, direction='in'): + for rule in Rule.portforwards(host=self): forward = { 'proto': rule.proto, 'private': rule.dport, 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 <ory.mate@ik.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\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 "<label>Disk:</label> %s" +msgstr "<label>Lemez:</label> %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 "<strong>%(count)s</strong> more" msgstr "még <strong>%(count)s</strong>" @@ -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 <strong>(optional)</strong>" msgstr "Módosítsa a virtuális gépet <strong>(igény szerint)</strong>" -#: 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 <strong>(optional)</strong>" msgstr "Törölje a virtális gépet <strong>(ha szükséges)</strong>" -#: 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 <strong>CTRL</" "strong> key." @@ -1642,7 +1739,7 @@ msgstr "" "Több virtuális gépet is kiválaszthat a <strong>CTRL</strong> 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 <strong>SHIFT</strong> 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 <strong>%(disk)s</strong> from <strong>" +"%(app)s</strong>?" +msgstr "" +"Biztosan eltávolítja a(z) <strong>%(disk)s</strong> 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 <strong>%(vm)s</strong>?" msgstr "" "Biztosan eltávolítja az interfészt a(z) <strong>%(vm)s</strong> 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 <strong>%(disk)s</strong> from <strong>" -"%(app)s</strong>?" -msgstr "" -"Biztosan eltávolítja a(z) <strong>%(disk)s</strong> 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. <a href=" "\"%(token)s\" class=\"btn btn-success btn-small\">Elfogadás</a>" -#: 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 <a href=\"%(url)s\">%(instance)s</a> is going to expire. It " @@ -5370,7 +5593,7 @@ msgstr "" "kerül. Kérjük, <a href=\"%(token)s\">újítsa meg</a> vagy <a href=\"%(url)s" "\">törölje</a> 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 "" "<a href=\"%(url)s\">%(instance)s</a> 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 <ory.mate@ik.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\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/network/views.py b/circle/network/views.py index 26f6d76..7193035 100644 --- a/circle/network/views.py +++ b/circle/network/views.py @@ -655,12 +655,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, def get_context_data(self, **kwargs): context = super(VlanDetail, self).get_context_data(**kwargs) - - q = Host.objects.filter(interface__in=Interface.objects.filter( - vlan=self.object - )) - - context['host_list'] = SmallHostTable(q) + context['host_list'] = SmallHostTable(self.object.host_set.all()) context['vlan_vid'] = self.kwargs.get('vid') context['acl'] = AclUpdateView.get_acl_data( self.object, self.request.user, 'network.vlan-acl') diff --git a/circle/storage/models.py b/circle/storage/models.py index 9809b6f..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 diff --git a/circle/vm/models/instance.py b/circle/vm/models/instance.py index cc69d5a..6f2aa96 100644 --- a/circle/vm/models/instance.py +++ b/circle/vm/models/instance.py @@ -817,8 +817,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, return acts def get_merged_activities(self, user=None): - whitelist = ("create_disk", "download_disk", "attach_disk", - "detach_disk", ) + whitelist = ("create_disk", "download_disk", + "add_port", "remove_port", + "attach_disk", "detach_disk", ) acts = self.get_activities(user) merged_acts = [] latest = None diff --git a/circle/vm/models/node.py b/circle/vm/models/node.py index bf82ccc..81710db 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): @@ -288,6 +290,11 @@ class Node(OperatedMixin, TimeStampedModel): @property @node_available + def driver_version(self): + return self.info.get('driver_version') + + @property + @node_available def cpu_usage(self): return self.monitor_info.get('cpu.percent') / 100 diff --git a/circle/vm/operations.py b/circle/vm/operations.py index cd8fef6..2b3948f 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -16,15 +16,22 @@ # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. 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.core.exceptions import PermissionDenied, SuspiciousOperation 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 @@ -43,9 +50,11 @@ from .models import ( Instance, InstanceActivity, InstanceTemplate, Interface, Node, NodeActivity, pwgen ) -from .tasks import agent_tasks, local_agent_tasks, vm_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__) @@ -162,6 +171,30 @@ class RemoteInstanceOperation(RemoteOperationMixin, InstanceOperation): 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): id = 'add_interface' @@ -198,7 +231,8 @@ class AddInterfaceOperation(InstanceOperation): 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"), @@ -264,6 +298,11 @@ class ResizeDiskOperation(RemoteInstanceOperation): 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): @@ -280,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) @@ -294,6 +332,10 @@ 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"]: self.instance._attach_disk(parent_activity=activity, disk=disk) @@ -324,10 +366,14 @@ class DeployOperation(InstanceOperation): "deployed to node: %(node)s"), node=self.instance.node) - def _operation(self, activity): + def _operation(self, activity, node=None): # Allocate VNC port and host node self.instance.allocate_vnc_port() - self.instance.allocate_node() + if node is not None: + self.instance.node = node + self.instance.save() + else: + self.instance.allocate_node() # Deploy virtual images self.instance._deploy_disks(parent_activity=activity) @@ -462,8 +508,8 @@ class DestroyOperation(InstanceOperation): 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', ) @@ -560,6 +606,41 @@ class RemoveInterfaceOperation(InstanceOperation): @register_operation +class RemovePortOperation(InstanceOperation): + id = 'remove_port' + name = _("close port") + description = _("Close the specified port.") + concurrency_check = False + required_perms = ('vm.config_ports', ) + + def _operation(self, activity, rule): + interface = rule.host.interface_set.get() + if interface.instance != self.instance: + raise SuspiciousOperation() + activity.readable_name = create_readable( + ugettext_noop("close %(proto)s/%(port)d on %(host)s"), + proto=rule.proto, port=rule.dport, host=rule.host) + rule.delete() + + +@register_operation +class AddPortOperation(InstanceOperation): + id = 'add_port' + name = _("open port") + description = _("Open the specified port.") + concurrency_check = False + required_perms = ('vm.config_ports', ) + + def _operation(self, activity, host, proto, port): + if host.interface_set.get().instance != self.instance: + raise SuspiciousOperation() + host.add_port(proto, private=port) + activity.readable_name = create_readable( + ugettext_noop("open %(proto)s/%(port)d on %(host)s"), + proto=proto, port=port, host=host) + + +@register_operation class RemoveDiskOperation(InstanceOperation): id = 'remove_disk' name = _("remove disk") @@ -633,7 +714,12 @@ class SaveAsTemplateOperation(InstanceOperation): disk.destroy() def _operation(self, activity, user, system, name=None, - with_shutdown=True, task=None, **kwargs): + 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, @@ -683,6 +769,13 @@ class SaveAsTemplateOperation(InstanceOperation): tmpl = InstanceTemplate(**params) tmpl.full_clean() # Avoiding database errors. tmpl.save() + # Copy traits from the VM instance + tmpl.req_traits.add(*self.instance.req_traits.all()) + if clone: + tmpl.clone_acl(self.instance.template) + # Add permission for the original owner of the template + tmpl.set_level(self.instance.template.owner, 'owner') + tmpl.set_level(user, 'owner') try: tmpl.disks.add(*self.disks) # create interface templates @@ -740,7 +833,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): @@ -811,6 +904,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 @@ -1046,6 +1140,7 @@ class ActivateOperation(NodeOperation): def _operation(self): self.node.enabled = True self.node.schedule_enabled = True + self.node.get_info(invalidate_cache=True) self.node.save() @@ -1067,6 +1162,7 @@ class PassivateOperation(NodeOperation): def _operation(self): self.node.enabled = True self.node.schedule_enabled = False + self.node.get_info(invalidate_cache=True) self.node.save() @@ -1125,13 +1221,27 @@ class RecoverOperation(InstanceOperation): except Instance.InstanceDestroyedError: pass - def _operation(self): - for disk in self.instance.disks.all(): - disk.destroyed = None - disk.restore() - disk.save() - self.instance.destroyed_at = None - self.instance.save() + def _operation(self, user, activity): + with activity.sub_activity( + 'recover_instance', + readable_name=ugettext_noop("recover instance")): + self.instance.destroyed_at = None + for disk in self.instance.disks.all(): + disk.destroyed = None + disk.restore() + disk.save() + self.instance.status = 'PENDING' + self.instance.save() + + try: + self.instance.renew(parent_activity=activity) + except: + pass + + if self.instance.template: + for net in self.instance.template.interface_set.all(): + self.instance.add_interface( + parent_activity=activity, user=user, vlan=net.vlan) @register_operation @@ -1161,27 +1271,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): +class PasswordResetOperation(RemoteAgentOperation): id = 'password_reset' name = _("password reset") description = _("Generate and set a new login password on the virtual " @@ -1190,17 +1281,236 @@ 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): id = 'mount_store' name = _("mount store") diff --git a/circle/vm/tasks/local_agent_tasks.py b/circle/vm/tasks/local_agent_tasks.py index f5a678b..90e1134 100644 --- a/circle/vm/tasks/local_agent_tasks.py +++ b/circle/vm/tasks/local_agent_tasks.py @@ -15,226 +15,26 @@ # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. -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, append, - change_ip, update_legacy) -from firewall.models import Host - -import time -import os -from base64 import encodestring -from hashlib import md5 -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_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', '') - - -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', '') +from manager.mancelery import celery @celery.task def agent_started(vm, version=None, system=None): - from vm.models import Instance, InstanceActivity + 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) 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, system, settings.AGENT_VERSION) - 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 - from vm.models.activity import ActivityInProgressError 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') - try: - with act.sub_activity('stopping', - readable_name=ugettext_noop('stopping')): - pass - except ActivityInProgressError: + 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, system=None, version=None): - if act: - act = act.sub_activity( - 'update', - readable_name=create_readable( - ugettext_noop('update to %(version)s'), - version=settings.AGENT_VERSION)) - else: - act = instance.activity( - code_suffix='agent.update', - readable_name=create_readable( - ugettext_noop('update agent to %(version)s'), - version=settings.AGENT_VERSION)) - with act: - queue = instance.get_remote_queue_name("agent") - if system == "Windows": - executable = os.listdir(os.path.join(settings.AGENT_DIR, - "agent-win"))[0] - # executable = "agent-winservice-%(version)s.exe" % { - # 'version': version} - data = create_windows_tar() - elif system == "Linux": - executable = "" - data = create_linux_tar() - else: - executable = "" - # Legacy update method - return update_legacy.apply_async( - queue=queue, - args=(instance.vm_name, create_linux_tar()) - ).get(timeout=60) - - checksum = md5(data).hexdigest() - chunk_size = 1024 * 1024 - chunk_number = 0 - index = 0 - filename = version + ".tar" - while True: - chunk = data[index:index+chunk_size] - if chunk: - 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: - update.apply_async( - queue=queue, - args=(instance.vm_name, filename, executable, checksum) - ).get(timeout=60) - break diff --git a/requirements/local.txt b/requirements/local.txt index 0234d08..8a10ecc 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -2,4 +2,5 @@ -r base.txt coverage==3.7.1 django-debug-toolbar==1.1 +django-rosetta==0.7.4 Sphinx==1.2.2