From 059d6cd772af7a89e95e97e73f45596332fef702 Mon Sep 17 00:00:00 2001
From: Stefano Alberto Russo <stefano.russo@gmail.com>
Date: Tue, 19 May 2020 20:03:08 +0200
Subject: [PATCH] Added the task direct link, and decoupled the tunnel creation
 part from the task view.

---
 .../webapp/code/rosetta/core_app/models.py    |  6 +-
 .../core_app/templates/components/task.html   | 15 +++-
 .../webapp/code/rosetta/core_app/utils.py     | 68 +++++++++++++++
 .../webapp/code/rosetta/core_app/views.py     | 84 ++++---------------
 services/webapp/code/rosetta/urls.py          |  3 +
 5 files changed, 105 insertions(+), 71 deletions(-)

diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py
index 6d4da2e..cd16cc0 100644
--- a/services/webapp/code/rosetta/core_app/models.py
+++ b/services/webapp/code/rosetta/core_app/models.py
@@ -329,11 +329,15 @@ class Task(models.Model):
     def __str__(self):
         return str('Task "{}" of user "{}" running on "{}" in status "{}" created at "{}"'.format(self.name, self.user, self.computing, self.status, self.created))
 
-    @ property
+    @property
     def color(self):
         string_int_hash = hash_string_to_int(self.name)
         color_map_index = string_int_hash % len(color_map)
         return color_map[color_map_index]
+    
+    @property
+    def direct_link(self):
+        return '{}/t/{}'.format(settings.DJANGO_PUBLIC_HTTP_HOST, self.id)
 
 
 
diff --git a/services/webapp/code/rosetta/core_app/templates/components/task.html b/services/webapp/code/rosetta/core_app/templates/components/task.html
index 572d5af..9779975 100644
--- a/services/webapp/code/rosetta/core_app/templates/components/task.html
+++ b/services/webapp/code/rosetta/core_app/templates/components/task.html
@@ -80,10 +80,8 @@
           </div>
 
 
-
           {% if data.task %}      
-
-         
+          <div style="float:left; max-width:450px">
           <table class="dashboard" style="margin:10px">
 
             <!-- <tr>
@@ -152,6 +150,12 @@
            </tr>
            {% endif %}
 
+           <tr>
+            <td style="padding-right:0"><b>Direct link</b>
+            <td>{% if task.status == "running" %}<a href="{{ task.direct_link }}">{{ task.direct_link }}</a>{% else %}N.A. (task not running) {% endif %}</td>
+           </tr>
+
+
            <!--  <tr>
             <td><b>Operations</b></td>
             
@@ -184,7 +188,10 @@
            -->
 
           </table>
-
+          <p style="margin-left:12px; font-size:0.9em; color:#484848">
+          <i class="fa fa-info-circle" style="color:#337ab7"></i> You can share a direct link with other people, but remember that if you have no authentication in place anyone will be able to access.
+          </p>
+          </div>
 
 
 
diff --git a/services/webapp/code/rosetta/core_app/utils.py b/services/webapp/code/rosetta/core_app/utils.py
index 2cb1455..63e565a 100644
--- a/services/webapp/code/rosetta/core_app/utils.py
+++ b/services/webapp/code/rosetta/core_app/utils.py
@@ -489,6 +489,74 @@ def hash_string_to_int(string):
 
 
 
+#================================
+#  Tunnel setup
+#================================
+
+def setup_tunnel(task):
+
+    # Importing here instead of on top avoids circular dependencies problems when loading booleanize in settings
+    from .models import Task, KeyPair, TaskStatuses
+    
+    # If there is no tunnel port allocated yet, find one
+    if not task.tunnel_port:
+
+        # Get a free port fot the tunnel:
+        allocated_tunnel_ports = []
+        for other_task in Task.objects.all():
+            if other_task.tunnel_port and not other_task.status in [TaskStatuses.exited, TaskStatuses.stopped]:
+                allocated_tunnel_ports.append(other_task.tunnel_port)
+
+        for port in range(7000, 7006):
+            if not port in allocated_tunnel_ports:
+                tunnel_port = port
+                break
+        if not tunnel_port:
+            logger.error('Cannot find a free port for the tunnel for task "{}"'.format(task))
+            raise ErrorMessage('Cannot find a free port for the tunnel to the task')
+
+        task.tunnel_port = tunnel_port
+        task.save()
+
+
+    # Check if the tunnel is active and if not create it
+    logger.debug('Checking if task "{}" has a running tunnel'.format(task))
+
+    out = os_shell('ps -ef | grep ":{}:{}:{}" | grep -v grep'.format(task.tunnel_port, task.ip, task.port), capture=True)
+
+    if out.exit_code == 0:
+        logger.debug('Task "{}" has a running tunnel, using it'.format(task))
+    else:
+        logger.debug('Task "{}" has no running tunnel, creating it'.format(task))
+
+        # Get user keys
+        user_keys = KeyPair.objects.get(user=task.user, default=True)
+
+        # Tunnel command
+        if task.computing.type == 'remotehop':           
+            
+            # Get computing params
+            first_host = task.computing.get_conf_param('first_host')
+            first_user = task.computing.get_conf_param('first_user')
+            #second_host = task.computing.get_conf_param('second_host')
+            #second_user = task.computing.get_conf_param('second_user')
+            #setup_command = task.computing.get_conf_param('setup_command')
+            #base_port = task.computing.get_conf_param('base_port')
+                     
+            tunnel_command= 'ssh -4 -i {} -o StrictHostKeyChecking=no -nNT -L 0.0.0.0:{}:{}:{} {}@{} & '.format(user_keys.private_key_file, task.tunnel_port, task.ip, task.port, first_user, first_host)
+
+        else:
+            tunnel_command= 'ssh -4 -o StrictHostKeyChecking=no -nNT -L 0.0.0.0:{}:{}:{} localhost & '.format(task.tunnel_port, task.ip, task.port)
+        
+        background_tunnel_command = 'nohup {} >/dev/null 2>&1 &'.format(tunnel_command)
+
+        # Log
+        logger.debug('Opening tunnel with command: {}'.format(background_tunnel_command))
+
+        # Execute
+        subprocess.Popen(background_tunnel_command, shell=True)
+
+
 
 
 
diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py
index bfd5e3d..0af6d4a 100644
--- a/services/webapp/code/rosetta/core_app/views.py
+++ b/services/webapp/code/rosetta/core_app/views.py
@@ -9,7 +9,7 @@ from django.http import HttpResponse, HttpResponseRedirect
 from django.contrib.auth.models import User
 from django.shortcuts import redirect
 from .models import Profile, LoginToken, Task, TaskStatuses, Container, Computing, KeyPair, ComputingSysConf, ComputingUserConf
-from .utils import send_email, format_exception, timezonize, os_shell, booleanize, debug_param, get_tunnel_host, random_username
+from .utils import send_email, format_exception, timezonize, os_shell, booleanize, debug_param, get_tunnel_host, random_username, setup_tunnel
 from .decorators import public_view, private_view
 from .exceptions import ErrorMessage
 
@@ -360,66 +360,11 @@ def tasks(request):
                 task.computing.manager.stop_task(task)
 
             elif action=='connect':
+                
+                # First ensure that the tunnel is setu up
+                setup_tunnel(task)
 
-                # If there is no tunnel port allocated yet, find one
-                if not task.tunnel_port:
-
-                    # Get a free port fot the tunnel:
-                    allocated_tunnel_ports = []
-                    for other_task in Task.objects.all():
-                        if other_task.tunnel_port and not other_task.status in [TaskStatuses.exited, TaskStatuses.stopped]:
-                            allocated_tunnel_ports.append(other_task.tunnel_port)
-
-                    for port in range(7000, 7006):
-                        if not port in allocated_tunnel_ports:
-                            tunnel_port = port
-                            break
-                    if not tunnel_port:
-                        logger.error('Cannot find a free port for the tunnel for task "{}"'.format(task))
-                        raise ErrorMessage('Cannot find a free port for the tunnel to the task')
-
-                    task.tunnel_port = tunnel_port
-                    task.save()
-
-
-                # Check if the tunnel is active and if not create it
-                logger.debug('Checking if task "{}" has a running tunnel'.format(task))
-
-                out = os_shell('ps -ef | grep ":{}:{}:{}" | grep -v grep'.format(task.tunnel_port, task.ip, task.port), capture=True)
-
-                if out.exit_code == 0:
-                    logger.debug('Task "{}" has a running tunnel, using it'.format(task))
-                else:
-                    logger.debug('Task "{}" has no running tunnel, creating it'.format(task))
-
-                    # Get user keys
-                    user_keys = KeyPair.objects.get(user=task.user, default=True)
-
-                    # Tunnel command
-                    if task.computing.type == 'remotehop':           
-                        
-                        # Get computing params
-                        first_host = task.computing.get_conf_param('first_host')
-                        first_user = task.computing.get_conf_param('first_user')
-                        #second_host = task.computing.get_conf_param('second_host')
-                        #second_user = task.computing.get_conf_param('second_user')
-                        #setup_command = task.computing.get_conf_param('setup_command')
-                        #base_port = task.computing.get_conf_param('base_port')
-                                 
-                        tunnel_command= 'ssh -4 -i {} -o StrictHostKeyChecking=no -nNT -L 0.0.0.0:{}:{}:{} {}@{} & '.format(user_keys.private_key_file, task.tunnel_port, task.ip, task.port, first_user, first_host)
-
-                    else:
-                        tunnel_command= 'ssh -4 -o StrictHostKeyChecking=no -nNT -L 0.0.0.0:{}:{}:{} localhost & '.format(task.tunnel_port, task.ip, task.port)
-                    
-                    background_tunnel_command = 'nohup {} >/dev/null 2>&1 &'.format(tunnel_command)
-
-                    # Log
-                    logger.debug('Opening tunnel with command: {}'.format(background_tunnel_command))
-
-                    # Execute
-                    subprocess.Popen(background_tunnel_command, shell=True)
-
-                # Ok, now redirect to the task through the tunnel
+                # Then, redirect to the task through the tunnel
                 tunnel_host = get_tunnel_host()
                 return redirect('http://{}:{}'.format(tunnel_host,task.tunnel_port))
 
@@ -965,16 +910,23 @@ def edit_computing_conf(request):
 
 
 
+#=========================
+#  Sharable link handler
+#=========================
 
+@public_view
+def sharable_link_handler(request, id):
 
+    # Get the task     
+    task = Task.objects.get(uuid__startswith=id)
 
+    # First ensure that the tunnel is setu up
+    setup_tunnel(task)
 
-
-
-
-
-
-
+    # Then, redirect to the task through the tunnel
+    tunnel_host = get_tunnel_host()
+    return redirect('http://{}:{}'.format(tunnel_host,task.tunnel_port))
+    
 
 
 
diff --git a/services/webapp/code/rosetta/urls.py b/services/webapp/code/rosetta/urls.py
index 99c2c84..36b16dc 100644
--- a/services/webapp/code/rosetta/urls.py
+++ b/services/webapp/code/rosetta/urls.py
@@ -50,6 +50,9 @@ urlpatterns = [
     url(r'^containers/$', core_app_views.containers),
     url(r'^add_container/$', core_app_views.add_container),
 
+    # Sharable link for tasks
+    url(r'^t/(?P<id>\w{0,36})/$', core_app_views.sharable_link_handler),
+
     # Modules
     path('admin/', admin.site.urls),
     path('api/v1/doc/', get_swagger_view(title="Swagger Documentation")),
-- 
GitLab