commit 605d23bf96a8cb6ca67e5a776e795ceb6065644d
Author: Kostas Papadimitriou <kpap@grnet.gr>
Date:   Wed Oct 31 11:29:22 2012 +0200

    Helpdesk suspend actions

diff --git a/snf-cyclades-app/synnefo/app_settings/default/helpdesk.py b/snf-cyclades-app/synnefo/app_settings/default/helpdesk.py
index f891447..c7cc264 100644
--- a/snf-cyclades-app/synnefo/app_settings/default/helpdesk.py
+++ b/snf-cyclades-app/synnefo/app_settings/default/helpdesk.py
@@ -4,3 +4,6 @@
 # Which is the cookie name that stores the token, leave it commented out
 # to use same value as UI_AUTH_COOKIE_NAME value.
 #HELPDESK_AUTH_COOKIE_NAME = UI_AUTH_COOKIE_NAME
+
+# Astakos groups which have access to helpdesk views
+#HELPDESK_PERMITTED_GROUPS = ['helpdesk']
diff --git a/snf-cyclades-app/synnefo/helpdesk/static/css/extra.css b/snf-cyclades-app/synnefo/helpdesk/static/css/extra.css
index 9751a83..b73148a 100644
--- a/snf-cyclades-app/synnefo/helpdesk/static/css/extra.css
+++ b/snf-cyclades-app/synnefo/helpdesk/static/css/extra.css
@@ -185,4 +185,9 @@ h4.expanded .badge				{ background-position: 185px 43px}
 
 h4 i 							{ margin-top: 4px; margin-right: 10px;}
 .container-fluid 				{ margin-left:auto; margin-right:auto; max-width:960px; }
-h3.info							{ cursor:default; color:#2956B2; text-align:center; font-size:16px; margin-bottom:0;}
\ No newline at end of file
+h3.info							{ cursor:default; color:#2956B2; text-align:center; font-size:16px; margin-bottom:0;}
+
+.vm-suspend-form { margin-top: 20px; text-align: right; }
+.vm-suspend-form form { margin-bottom: 0px; }
+.vm-suspend-form.suspended form input { background-color: #2956B2; color: #ffffff;}
+.vm-suspend-form form input { background-color: #F81A23; color: #ffffff;}
diff --git a/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/_suspend.html b/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/_suspend.html
new file mode 100644
index 0000000..21a2ed9
--- /dev/null
+++ b/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/_suspend.html
@@ -0,0 +1,11 @@
+{% if vm.suspended %}
+<form method="post" action="{% url helpdesk-suspend-vm-release vm_id=vm.pk %}">
+  <input type="hidden" name="token" value="{{ csrf_token }}" />
+  <input type="submit" value="RELEASE SUSPENSION" />
+</form>
+{% else %}
+<form method="post" action="{% url helpdesk-suspend-vm vm_id=vm.pk %}">
+  <input type="hidden" name="token" value="{{ csrf_token }}" />
+  <input type="submit" value="SUSPEND" />
+</form>
+{% endif %}
diff --git a/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html b/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html
index 71c473d..9bf1a90 100644
--- a/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html
+++ b/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html
@@ -1,95 +1,100 @@
 {% load helpdesk_tags %}
 <div class="object-anchor" id="vm-{{vm.pk}}"></div>
 <div class="vm-details object-details {{ rowcls }}">
-    <h4><em><img src="{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png" />{{ vm|get_os }}</em><i class="icon-tasks"></i>{{ vm.name }}<span class="badge">&nbsp;</span></h4>
-    {{ vm|vm_status_badge|safe }}
-    <span class="badge badge-inverse">ID: {{ vm.pk }}</span>
-    <span class="badge badge-inverse">{{ vm|vm_public_ip }}</span>
-    <span class="badge badge-inverse flavor">
-        <span class="cpu">{{ vm.flavor.cpu }}x</span>
-        <span class="ram">{{ vm.flavor.ram}}MB</span>
-        <span class="disk">{{ vm.flavor.disk }}GB</span>
+  <h4><em><img src="{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png" />{{ vm|get_os }}</em><i class="icon-tasks"></i>{{ vm.name }}<span class="badge">&nbsp;</span></h4>
+  {{ vm|vm_status_badge|safe }}
+  <span class="badge badge-inverse">ID: {{ vm.pk }}</span>
+  <span class="badge badge-inverse">{{ vm|vm_public_ip }}</span>
+  {% if vm.suspended %}
+  <span class="badge badge-important">SUSPENDED</span>
+  {% endif %}
+  <span class="badge badge-inverse flavor">
+    <span class="cpu">{{ vm.flavor.cpu }}x</span>
+    <span class="ram">{{ vm.flavor.ram}}MB</span>
+    <span class="disk">{{ vm.flavor.disk }}GB</span>
+  </span>
+  <div class="vm-details-content object-details-content">
 
-    </span>
-    <div class="vm-details-content object-details-content">
-        
-        <ul class="nav nav-tabs">
-		    <li class="active"><a href="#details{{ vm.pk }}" data-toggle="tab">Details</a></li>
-		    <li><a href="#metadata{{ vm.pk }}" data-toggle="tab">Metadata</a></li>
-		    <li><a href="#backend{{ vm.pk }}" data-toggle="tab">Backend info</a></li>
-		    <li><a href="#network{{ vm.pk }}" data-toggle="tab">Network interfaces</a></li>
-	    </ul>
-	    <div class="tab-content">
-			<div class="tab-pane active" id="details{{ vm.pk }}">
-				<dl class="dl-horizontal well">
-		            <dt>ID</dt><dd>{{ vm.pk }}</dd>
-		            <dt>Name</dt><dd>{{ vm.name }}</dd>
-		            <dt>User id</dt><dd>{{ vm.userid }}</dd>
-		            <dt>Created</dt><dd>{{ vm.created }} ({{ vm.created|timesince }} <strong>ago</strong>)</dd>
-		            <dt>Updated</dt><dd>{{ vm.updated }} ({{ vm.updated|timesince }} <strong>ago</strong>)</dd>
-		            <dt>Suspended</dt><dd>{{ vm.suspended }}</dd>
-		            <dt>Deleted</dt><dd>{{ vm.deleted }}</dd>
-		            <dt>Image id</dt><dd>{{ vm.imageid }}</dd>
-		            <dt>Flavor</dt><dd>{{ vm.flavor.cpu }},
-		                        {{ vm.flavor.disk }},
-		                        {{ vm.flavor.ram }},
-		                        {{ vm.flavor.disk_template }}</dd>
-		        </dl>
-			</div>
-			<div class="tab-pane" id="metadata{{ vm.pk }}">
-				<dl class="dl-horizontal well">
-		            {% for meta in vm.metadata.all %}  
-			            <dt>{{ meta.meta_key }}</dt><dd>{{ meta.meta_value }}</dd>
-		            {% empty %}
-		            <dt>No metadata</dt>
-		            {% endfor %}
-		        </dl>
-			</div>
-			<div class="tab-pane" id="backend{{ vm.pk }}">
-				<dl class="dl-horizontal well">
-		            <dt>Action</dt><dd>{{ vm.get_action_display }} ({{ vm.action }})</dd>
-		            <dt>Operstate</dt><dd>{{ vm.get_operstate_display }} ({{ vm.operstate }})</dd>
-		            <dt>Backend job id</dt><dd>{{ vm.backendjobid }}</dd>
-		            <dt>Backend op code</dt><dd>{{ vm.get_backendopcode_display }} ({{ vm.backendopcode }})</dd>
-		            <dt>Backend log msg</dt><dd>{{ vm.backendlogmsg }}</dd>
-		            <dt>Build backendjobstatus</dt><dd>{{ vm.backendjobstatus }}</dd>
-		            <dt>Build percentage</dt><dd>{{ vm.buildpercentage }}</dd>
-		        </dl>
-			</div>
-			<div class="tab-pane" id="network{{ vm.pk }}">
-				<table class="table well">
-		            <thead>
-		                <td>ID</td>
-		                <td>Network (ID)</td>
-		                <td>Created</td>
-		                <td>Updated</td>
-		                <td>Index</td>
-		                <td>MAC</td>
-		                <td>IPv4</td>
-		                <td>IPv6</td>
-		                <td>Firewall</td>
-		            </thead>
-		            <tbody>
-		                {% for nic in vm.nics.all %}
-		                <tr>
-		                    <td>{{ nic.pk }}</td>
-		                    <td>{{ nic.network }} ({{ nic.network.pk }})</td>
-		                    <td>{{ nic.created }}</td>
-		                    <td>{{ nic.updated }}</td>
-		                    <td>{{ nic.index }}</td>
-		                    <td>{{ nic.mac }}</td>
-		                    <td>{{ nic.ipv4 }}</td>
-		                    <td>{{ nic.ipv6 }}</td>
-		                    <td>{{ nic.get_firewall_profile_display }} ({{nic.firewall_profile}})</td>
-		                </tr>
-		                {% empty %}
-		                <tr>
-		                    <td colspan=9>No network interface available</td>
-		                </tr>
-		                {% endfor %}
-		            </tbody>
-		        </table>
-			</div>
-		</div>        
-    </div>
+    <ul class="nav nav-tabs">
+      <li class="active"><a href="#details{{ vm.pk }}" data-toggle="tab">Details</a></li>
+      <li><a href="#metadata{{ vm.pk }}" data-toggle="tab">Metadata</a></li>
+      <li><a href="#backend{{ vm.pk }}" data-toggle="tab">Backend info</a></li>
+      <li><a href="#network{{ vm.pk }}" data-toggle="tab">Network interfaces</a></li>
+    </ul>
+    <div class="tab-content">
+      <div class="tab-pane active" id="details{{ vm.pk }}">
+        <dl class="dl-horizontal well">
+          <dt>ID</dt><dd>{{ vm.pk }}</dd>
+          <dt>Name</dt><dd>{{ vm.name }}</dd>
+          <dt>User id</dt><dd>{{ vm.userid }}</dd>
+          <dt>Created</dt><dd>{{ vm.created }} ({{ vm.created|timesince }} <strong>ago</strong>)</dd>
+          <dt>Updated</dt><dd>{{ vm.updated }} ({{ vm.updated|timesince }} <strong>ago</strong>)</dd>
+          <dt>Suspended</dt><dd>{{ vm.suspended }}</dd>
+          <dt>Deleted</dt><dd>{{ vm.deleted }}</dd>
+          <dt>Image id</dt><dd>{{ vm.imageid }}</dd>
+          <dt>Flavor</dt><dd>{{ vm.flavor.cpu }},
+          {{ vm.flavor.disk }},
+          {{ vm.flavor.ram }},
+          {{ vm.flavor.disk_template }}</dd>
+        </dl>
+      </div>
+      <div class="tab-pane" id="metadata{{ vm.pk }}">
+        <dl class="dl-horizontal well">
+          {% for meta in vm.metadata.all %}  
+          <dt>{{ meta.meta_key }}</dt><dd>{{ meta.meta_value }}</dd>
+          {% empty %}
+          <dt>No metadata</dt>
+          {% endfor %}
+        </dl>
+      </div>
+      <div class="tab-pane" id="backend{{ vm.pk }}">
+        <dl class="dl-horizontal well">
+          <dt>Action</dt><dd>{{ vm.get_action_display }} ({{ vm.action }})</dd>
+          <dt>Operstate</dt><dd>{{ vm.get_operstate_display }} ({{ vm.operstate }})</dd>
+          <dt>Backend job id</dt><dd>{{ vm.backendjobid }}</dd>
+          <dt>Backend op code</dt><dd>{{ vm.get_backendopcode_display }} ({{ vm.backendopcode }})</dd>
+          <dt>Backend log msg</dt><dd>{{ vm.backendlogmsg }}</dd>
+          <dt>Build backendjobstatus</dt><dd>{{ vm.backendjobstatus }}</dd>
+          <dt>Build percentage</dt><dd>{{ vm.buildpercentage }}</dd>
+        </dl>
+      </div>
+      <div class="tab-pane" id="network{{ vm.pk }}">
+        <table class="table well">
+          <thead>
+            <td>ID</td>
+            <td>Network (ID)</td>
+            <td>Created</td>
+            <td>Updated</td>
+            <td>Index</td>
+            <td>MAC</td>
+            <td>IPv4</td>
+            <td>IPv6</td>
+            <td>Firewall</td>
+          </thead>
+          <tbody>
+            {% for nic in vm.nics.all %}
+            <tr>
+              <td>{{ nic.pk }}</td>
+              <td>{{ nic.network }} ({{ nic.network.pk }})</td>
+              <td>{{ nic.created }}</td>
+              <td>{{ nic.updated }}</td>
+              <td>{{ nic.index }}</td>
+              <td>{{ nic.mac }}</td>
+              <td>{{ nic.ipv4 }}</td>
+              <td>{{ nic.ipv6 }}</td>
+              <td>{{ nic.get_firewall_profile_display }} ({{nic.firewall_profile}})</td>
+            </tr>
+            {% empty %}
+            <tr>
+              <td colspan=9>No network interface available</td>
+            </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    </div>        
+  </div>
+  <div class="vm-suspend-form {% if vm.suspended %}suspended{% endif %}">
+    {% include "helpdesk/_suspend.html" %}
+  </div>
 </div>
diff --git a/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py b/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py
index caa15c6..953a6b8 100644
--- a/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py
+++ b/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py
@@ -43,7 +43,7 @@ def network_deleted_badge(network):
     Return a span badge styled based on the vm current status
     """
     deleted_badge = ""
-    if network.state == "DELETED":
+    if network.deleted:
         deleted_badge = '<span class="badge badge-important">Deleted</span>'
     return deleted_badge
 
@@ -55,7 +55,7 @@ def get_os(vm):
         return vm.metadata.filter(meta_key="OS").get().meta_value
     except:
         return "unknown"
-    
+
 get_os.is_safe = True
 
 @register.filter(name="network_vms")
@@ -64,7 +64,7 @@ def network_vms(network, account):
     for nic in network.nics.filter(machine__userid=account):
         vms.append(nic.machine)
     return vms
-    
+
 network_vms.is_safe = True
 
 @register.filter(name="network_nics")
@@ -73,5 +73,5 @@ def network_nics(network, account):
     for nic in network.nics.filter(machine__userid=account):
         vms.append(nic)
     return vms
-    
-network_nics.is_safe = True
\ No newline at end of file
+
+network_nics.is_safe = True
diff --git a/snf-cyclades-app/synnefo/helpdesk/urls.py b/snf-cyclades-app/synnefo/helpdesk/urls.py
index 88ff9c9..a135b7a 100644
--- a/snf-cyclades-app/synnefo/helpdesk/urls.py
+++ b/snf-cyclades-app/synnefo/helpdesk/urls.py
@@ -2,6 +2,11 @@ from django.conf.urls.defaults import patterns, url
 
 urlpatterns = patterns('',
     url(r'^$', 'synnefo.helpdesk.views.index', name='helpdesk-index'),
+    url(r'^suspend/(?P<vm_id>[0-9]+)$', 'synnefo.helpdesk.views.suspend_vm',
+        name='helpdesk-suspend-vm'),
+    url(r'^suspend_release/(?P<vm_id>[0-9]+)$',
+        'synnefo.helpdesk.views.suspend_vm_release',
+        name='helpdesk-suspend-vm-release'),
     url(r'^api/users', 'synnefo.helpdesk.views.user_list',
         name='helpdesk-userslist'),
     url(r'^(?P<account_or_ip>.*)$', 'synnefo.helpdesk.views.account',
diff --git a/snf-cyclades-app/synnefo/helpdesk/views.py b/snf-cyclades-app/synnefo/helpdesk/views.py
index 7c5ba32..2f82a6e 100644
--- a/snf-cyclades-app/synnefo/helpdesk/views.py
+++ b/snf-cyclades-app/synnefo/helpdesk/views.py
@@ -1,4 +1,38 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
 import re
+import logging
 
 from itertools import chain
 
@@ -8,13 +42,17 @@ from django.db.models import get_apps
 from django.conf import settings
 from django.core.exceptions import PermissionDenied
 from django.db.models import Q
-from django.http import Http404, HttpResponse
+from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.utils import simplejson as json
+from django.core.urlresolvers import reverse
+
 from urllib import unquote
 
 from synnefo.lib.astakos import get_user
 from synnefo.db.models import *
 
+logger = logging.getLogger(__name__)
+
 IP_SEARCH_REGEX = re.compile('([0-9]+)(?:\.[0-9]+){3}')
 
 def get_token_from_cookie(request, cookiename):
@@ -33,15 +71,31 @@ def get_token_from_cookie(request, cookiename):
     return None
 
 
-# TODO: here we mix ui setting with helpdesk settings
-# if sometime in the future helpdesk gets splitted from the
-# cyclades api code this should change and helpdesk should provide
-# its own setting HELPDESK_AUTH_COOKIE_NAME.
-AUTH_COOKIE = getattr(settings, 'UI_AUTH_COOKIE_NAME', getattr(settings,
-    'HELPDESK_AUTH_COOKIE_NAME', '_pithos2_a'))
+AUTH_COOKIE_NAME = getattr(settings, 'HELPDESK_AUTH_COOKIE_NAME', getattr(settings,
+    'UI_AUTH_COOKIE_NAME', '_pithos2_a'))
+PERMITTED_GROUPS = getattr(settings, 'HELPDESK_PERMITTED_GROUPS',
+                                    ['helpdesk'])
+SHOW_DELETED_VMS = getattr(settings, 'HELPDESK_SHOW_DELETED_VMS', False)
+
+
+def token_check(func):
+    """
+    Mimic csrf security check using user auth token.
+    """
+    def wrapper(request, *args, **kwargs):
+        if not hasattr(request, 'user'):
+            raise PermissionDenied
+
+        token = request.POST.get('token', None)
+        if token and token != request.user.get('auth_token', None):
+            return func(request, *args, **kwargs)
+
+        raise PermissionDenied
+
+    return wrapper
 
 
-def helpdesk_user_required(func, groups=['helpdesk']):
+def helpdesk_user_required(func, permitted_groups=PERMITTED_GROUPS):
     """
     Django view wrapper that checks if identified request user has helpdesk
     permissions (exists in helpdesk group)
@@ -51,7 +105,7 @@ def helpdesk_user_required(func, groups=['helpdesk']):
         if not HELPDESK_ENABLED:
             raise Http404
 
-        token = get_token_from_cookie(request, AUTH_COOKIE)
+        token = get_token_from_cookie(request, AUTH_COOKIE_NAME)
         get_user(request, settings.ASTAKOS_URL, fallback_token=token)
         if hasattr(request, 'user') and request.user:
             groups = request.user.get('groups', [])
@@ -59,12 +113,17 @@ def helpdesk_user_required(func, groups=['helpdesk']):
             if not groups:
                 raise PermissionDenied
 
+            has_perm = False
             for g in groups:
-                if not g in groups:
-                    raise PermissionDenied
+                if g in permitted_groups:
+                    has_perm = True
+
+            if not has_perm:
+                raise PermissionDenied
         else:
             raise PermissionDenied
 
+        logging.debug("User %s accessed helpdesk view" % (request.user_uniq))
         return func(request, *args, **kwargs)
 
     return wrapper
@@ -91,6 +150,8 @@ def account(request, account_or_ip):
     Account details view.
     """
 
+    show_deleted = bool(int(request.GET.get('deleted', SHOW_DELETED_VMS)))
+
     account_exists = True
     vms = []
     networks = []
@@ -104,12 +165,18 @@ def account(request, account_or_ip):
         except NetworkInterface.DoesNotExist:
             account_exists = False
     else:
-        # all user vms
-        vms = VirtualMachine.objects.filter(userid=account).order_by('deleted')
+        filter_extra = {}
+        if not show_deleted:
+            filter_extra['deleted'] = False
 
+        # all user vms
+        vms = VirtualMachine.objects.filter(userid=account,
+                                            **filter_extra).order_by('deleted')
         # return all user private and public networks
-        public_networks = Network.objects.filter(public=True).order_by('state')
-        private_networks = Network.objects.filter(userid=account).order_by('state')
+        public_networks = Network.objects.filter(public=True,
+                                                 **filter_extra).order_by('state')
+        private_networks = Network.objects.filter(userid=account,
+                                                 **filter_extra).order_by('state')
         networks = list(public_networks) + list(private_networks)
 
         if vms.count() == 0 and private_networks.count() == 0:
@@ -120,6 +187,7 @@ def account(request, account_or_ip):
         'is_ip': is_ip,
         'account': account,
         'vms': vms,
+        'csrf_token': request.user['auth_token'],
         'networks': networks,
         'UI_MEDIA_URL': settings.UI_MEDIA_URL
     }
@@ -129,6 +197,26 @@ def account(request, account_or_ip):
 
 
 @helpdesk_user_required
+@token_check
+def suspend_vm(request, vm_id):
+    vm = VirtualMachine.objects.get(pk=vm_id)
+    vm.suspended = True
+    vm.save()
+    account = vm.userid
+    return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))
+
+
+@helpdesk_user_required
+@token_check
+def suspend_vm_release(request, vm_id):
+    vm = VirtualMachine.objects.get(pk=vm_id)
+    vm.suspended = False
+    vm.save()
+    account = vm.userid
+    return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))
+
+
+@helpdesk_user_required
 def user_list(request):
     """
     Return a json list of users based on the prefix provided. Prefix
