diff --git a/circle/dashboard/static/dashboard/vm-common.js b/circle/dashboard/static/dashboard/vm-common.js index bc3f0ef..fb4d0b6 100644 --- a/circle/dashboard/static/dashboard/vm-common.js +++ b/circle/dashboard/static/dashboard/vm-common.js @@ -3,7 +3,7 @@ $(function() { /* vm operations */ - $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) { + $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) { var icon = $(this).children("i").addClass('fa-spinner fa-spin'); $.ajax({ diff --git a/circle/dashboard/templates/dashboard/vm-detail/home.html b/circle/dashboard/templates/dashboard/vm-detail/home.html index 536b5eb..1ec326f 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/home.html +++ b/circle/dashboard/templates/dashboard/vm-detail/home.html @@ -100,6 +100,20 @@ {% endif %} </dd> </dl> + + {% if op.mount_store %} + <strong>{% trans "Store" %}</strong> + <p> + {{ op.mount_store.description }} + </p> + <div class="operation-wrapper"> + <a href="{{ op.mount_store.get_url }}" class="btn btn-info btn-xs operation" + {% if op.mount_store.disabled %}disabled{% endif %}> + <i class="fa fa-{{op.mount_store.icon}}"></i> + {{ op.mount_store.name }} + </a> + </div> + {% endif %} </div> <div class="col-md-8"> {% if graphite_enabled %} diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index 1924e37..33d3954 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -966,6 +966,10 @@ vm_ops = OrderedDict([ ('password_reset', VmOperationView.factory( op='password_reset', icon='unlock', effect='warning', show_in_toolbar=False, wait_for_result=0.5, with_reload=True)), + ('mount_store', VmOperationView.factory( + op='mount_store', icon='briefcase', effect='info', + show_in_toolbar=False, + )), ]) diff --git a/circle/vm/models/instance.py b/circle/vm/models/instance.py index 236a8d0..4335a8b 100644 --- a/circle/vm/models/instance.py +++ b/circle/vm/models/instance.py @@ -293,6 +293,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, message = ugettext_noop( "Instance %(instance)s has already been destroyed.") + class NoAgentError(InstanceError): + message = ugettext_noop( + "No agent software is running on instance %(instance)s.") + class WrongStateError(InstanceError): message = ugettext_noop( "Current state (%(state)s) of instance %(instance)s is " diff --git a/circle/vm/operations.py b/circle/vm/operations.py index b3f3412..5f7322e 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -19,10 +19,12 @@ from __future__ import absolute_import, unicode_literals from logging import getLogger from re import search from string import ascii_lowercase +from urlparse import urlsplit from django.core.exceptions import PermissionDenied from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ugettext_noop +from django.conf import settings from sizefield.utils import filesizeformat @@ -41,6 +43,8 @@ from .models import ( ) from .tasks import agent_tasks +from dashboard.store_api import Store, NoStoreException + logger = getLogger(__name__) @@ -934,8 +938,27 @@ class ResourcesOperation(InstanceOperation): register_operation(ResourcesOperation) -class PasswordResetOperation(InstanceOperation): - activity_code_suffix = 'Password reset' +class EnsureAgentMixin(object): + accept_states = ('RUNNING', ) + + def check_precond(self): + super(EnsureAgentMixin, self).check_precond() + + last_boot_time = self.instance.activity_log.filter( + succeeded=True, activity_code__in=( + "vm.Instance.deploy", "vm.Instance.reset", + "vm.Instance.reboot")).latest("finished").finished + + try: + InstanceActivity.objects.filter( + activity_code="vm.Instance.agent.starting", + started__gt=last_boot_time).latest("started") + except InstanceActivity.DoesNotExist: # no agent since last boot + raise self.instance.NoAgentError(self.instance) + + +class PasswordResetOperation(EnsureAgentMixin, InstanceOperation): + activity_code_suffix = 'password_reset' id = 'password_reset' name = _("password reset") description = _("Generate and set a new login password on the virtual " @@ -945,7 +968,6 @@ class PasswordResetOperation(InstanceOperation): "it.") acl_level = "owner" required_perms = () - accept_states = ('RUNNING', ) def _operation(self): self.instance.pw = pwgen() @@ -956,3 +978,34 @@ class PasswordResetOperation(InstanceOperation): register_operation(PasswordResetOperation) + + +class MountStoreOperation(EnsureAgentMixin, InstanceOperation): + activity_code_suffix = 'mount_store' + id = 'mount_store' + name = _("mount store") + description = _( + "This operation attaches your personal file store. Other users who " + "have access to this machine can see these files as well." + ) + acl_level = "owner" + required_perms = () + + def check_auth(self, user): + super(MountStoreOperation, self).check_auth(user) + try: + Store(user) + except NoStoreException: + raise PermissionDenied # not show the button at all + + def _operation(self): + inst = self.instance + queue = self.instance.get_remote_queue_name("agent") + host = urlsplit(settings.STORE_URL).hostname + username = Store(inst.owner).username + password = inst.owner.profile.smb_password + agent_tasks.mount_store.apply_async( + queue=queue, args=(inst.vm_name, host, username, password)) + + +register_operation(MountStoreOperation)